Remove orphaned apps/trust frontend

Compliance portal already embeds and serves the public SPA, so the
old @probo/trust app was unused. Drop its Makefile/CI stubs, Relay
project, CORS port, and the helper that only it imported.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-22 14:11:51 +02:00
parent a77ada6f27
commit 00f5d0c4ba
92 changed files with 15 additions and 5567 deletions

View File

@@ -18,7 +18,7 @@
# ── Base URL & ports ────────────────────────────────────────────────── # ── Base URL & ports ──────────────────────────────────────────────────
# PROBOD_BASE_URL=http://localhost:8080 # PROBOD_BASE_URL=http://localhost:8080
# PROBOD_API_ADDR=:8080 # PROBOD_API_ADDR=:8080
# PROBOD_API_CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173,http://localhost:5174,http://localhost:5175 # PROBOD_API_CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173,http://localhost:5174
# PROBOD_TRUST_CENTER_HTTP_ADDR=:10080 # PROBOD_TRUST_CENTER_HTTP_ADDR=:10080
# PROBOD_TRUST_CENTER_HTTPS_ADDR=:443 # PROBOD_TRUST_CENTER_HTTPS_ADDR=:443
# PROBOD_TRUST_CENTER_BASE_DOMAIN=probopage.localhost # PROBOD_TRUST_CENTER_BASE_DOMAIN=probopage.localhost

View File

@@ -245,10 +245,9 @@ jobs:
node: "false" node: "false"
- name: "Create placeholder dist files" - name: "Create placeholder dist files"
run: | run: |
mkdir -p apps/console/dist apps/compliance-portal/dist apps/trust/dist packages/emails/dist mkdir -p apps/console/dist apps/compliance-portal/dist packages/emails/dist
echo dev-server > apps/console/dist/index.html echo dev-server > apps/console/dist/index.html
echo dev-server > apps/compliance-portal/dist/index.html echo dev-server > apps/compliance-portal/dist/index.html
echo dev-server > apps/trust/dist/index.html
echo dev-server > packages/emails/dist/placeholder echo dev-server > packages/emails/dist/placeholder
- name: "Generate Go code" - name: "Generate Go code"
run: | run: |
@@ -310,10 +309,9 @@ jobs:
- uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0 - uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0
- name: "Create placeholder dist files" - name: "Create placeholder dist files"
run: | run: |
mkdir -p apps/console/dist apps/compliance-portal/dist apps/trust/dist packages/emails/dist mkdir -p apps/console/dist apps/compliance-portal/dist packages/emails/dist
echo dev-server > apps/console/dist/index.html echo dev-server > apps/console/dist/index.html
echo dev-server > apps/compliance-portal/dist/index.html echo dev-server > apps/compliance-portal/dist/index.html
echo dev-server > apps/trust/dist/index.html
echo dev-server > packages/emails/dist/placeholder echo dev-server > packages/emails/dist/placeholder
- name: "Generate Go code" - name: "Generate Go code"
run: | run: |
@@ -371,7 +369,7 @@ jobs:
env: env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
for dir in apps/console apps/trust apps/compliance-portal packages/ui packages/eslint-config; do for dir in apps/console apps/compliance-portal packages/ui packages/eslint-config; do
(cd "$dir" && npx eslint . --concurrency 4 --format stylish 2>/dev/null) | \ (cd "$dir" && npx eslint . --concurrency 4 --format stylish 2>/dev/null) | \
reviewdog -f=eslint -reporter=github-pr-review -filter-mode=nofilter -name="eslint ($dir)" || true reviewdog -f=eslint -reporter=github-pr-review -filter-mode=nofilter -name="eslint ($dir)" || true
done done
@@ -389,10 +387,9 @@ jobs:
- uses: "./.github/actions/setup" - uses: "./.github/actions/setup"
- name: "Create placeholder dist files" - name: "Create placeholder dist files"
run: | run: |
mkdir -p apps/console/dist apps/compliance-portal/dist apps/trust/dist mkdir -p apps/console/dist apps/compliance-portal/dist
echo dev-server > apps/console/dist/index.html echo dev-server > apps/console/dist/index.html
echo dev-server > apps/compliance-portal/dist/index.html echo dev-server > apps/compliance-portal/dist/index.html
echo dev-server > apps/trust/dist/index.html
- run: "npm --workspace @probo/emails run build" - run: "npm --workspace @probo/emails run build"
- run: "make test" - run: "make test"
env: env:

View File

@@ -72,7 +72,6 @@ GENERATED= pkg/server/api/connect/v1/schema/schema.go \
EMBEDDED= apps/console/dist/index.html \ EMBEDDED= apps/console/dist/index.html \
apps/compliance-portal/dist/index.html \ apps/compliance-portal/dist/index.html \
apps/trust/dist/index.html \
@probo/emails @probo/emails
PROBOD_BIN_EXTRA_DEPS= PROBOD_BIN_EXTRA_DEPS=
@@ -138,7 +137,7 @@ go-lint: generate
$(GOLINTCMD) run ./... $(GOLINTCMD) run ./...
.PHONY: test .PHONY: test
test: generate apps/trust/dist/index.html test: generate
test: CGO_ENABLED=1 test: CGO_ENABLED=1
test: ## Run tests with race detection and coverage (usage: make test [MODULE=./pkg/some/module]) test: ## Run tests with race detection and coverage (usage: make test [MODULE=./pkg/some/module])
$(GO_TEST) $(if $(MODULE),$(MODULE),$(shell $(GO) list ./... | grep -v /e2e/)) $(GO_TEST) $(if $(MODULE),$(MODULE),$(shell $(GO) list ./... | grep -v /e2e/))
@@ -211,7 +210,7 @@ cfg/dev.yaml: bin/probod-bootstrap $(CFG_DEV_OAUTH2_KEY) compose/step-ca/certs/r
PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes"; \ PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes"; \
PROBOD_AUTH_COOKIE_SECURE=false; \ PROBOD_AUTH_COOKIE_SECURE=false; \
PROBOD_OAUTH2_SERVER_SIGNING_KEY="$$($(CAT) $(CFG_DEV_OAUTH2_KEY))"; \ PROBOD_OAUTH2_SERVER_SIGNING_KEY="$$($(CAT) $(CFG_DEV_OAUTH2_KEY))"; \
PROBOD_API_CORS_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:5173,http://localhost:5174,http://localhost:5175"; \ PROBOD_API_CORS_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:5173,http://localhost:5174"; \
PROBOD_PG_ADDR=localhost:5432; \ PROBOD_PG_ADDR=localhost:5432; \
PROBOD_PG_USERNAME=postgres; \ PROBOD_PG_USERNAME=postgres; \
PROBOD_PG_PASSWORD=postgres; \ PROBOD_PG_PASSWORD=postgres; \
@@ -387,7 +386,7 @@ fmt-go: ## Format Go code
clean: ## Clean the project (node_modules and build artifacts) clean: ## Clean the project (node_modules and build artifacts)
$(RM) -rf bin/* $(RM) -rf bin/*
$(RM) -rf node_modules $(RM) -rf node_modules
$(RM) -rf apps/{console,compliance-portal,trust}/{dist,node_modules} $(RM) -rf apps/{console,compliance-portal}/{dist,node_modules}
$(RM) -rf packages/emails/{dist,node_modules} $(RM) -rf packages/emails/{dist,node_modules}
$(RM) -rf sbom-docker.json sbom.json $(RM) -rf sbom-docker.json sbom.json
$(RM) -rf coverage.out coverage.html coverage-e2e.out coverage-e2e.html coverage-combined.out coverage-combined.html $(RM) -rf coverage.out coverage.html coverage-e2e.out coverage-e2e.html coverage-combined.out coverage-combined.html
@@ -443,7 +442,7 @@ compose/keycloak/probo-realm.json: compose/keycloak/probo-realm.json.tmpl compos
-e "s|PRIVATE_KEY_PLACEHOLDER|$$(awk 'NR==1 {printf "%s", $$0; next} {printf "\\\\n%s", $$0}' compose/keycloak/certs/private-key.pem)|g" \ -e "s|PRIVATE_KEY_PLACEHOLDER|$$(awk 'NR==1 {printf "%s", $$0; next} {printf "\\\\n%s", $$0}' compose/keycloak/certs/private-key.pem)|g" \
$@.tmpl > $@ $@.tmpl > $@
apps/console/dist/index.html apps/compliance-portal/dist/index.html apps/trust/dist/index.html: apps/console/dist/index.html apps/compliance-portal/dist/index.html:
$(MKDIR) $(dir $@) $(MKDIR) $(dir $@)
$(ECHO) dev-server > $@ $(ECHO) dev-server > $@

View File

@@ -1 +0,0 @@
VITE_API_URL=http://localhost:8080

View File

@@ -1,2 +0,0 @@
__generated__/*.graphql linguist-generated
__generated__/*.js linguist-generated

26
apps/trust/.gitignore vendored
View File

@@ -1,26 +0,0 @@
public/pdfjs
# Logs
old
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,2 +0,0 @@
# Trust app (compliance portal)

View File

@@ -1,52 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
(function () {
var m = location.pathname.match(/^\/trust\/[^/]+/);
var b = document.createElement("base");
b.href = m ? m[0] + "/" : "/";
document.head.appendChild(b);
})();
</script>
<!-- <link rel="apple-touch-icon-precomposed" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="/favicons/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="/favicons/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/favicons/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="/favicons/favicon-128.png" sizes="128x128" /> -->
<link rel="icon" id="favicon" href="{{if .FaviconURL}}{{.FaviconURL}}{{else}}/favicons/favicon.ico{{end}}">
<meta name="application-name" content="&nbsp;" />
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
<meta name="msapplication-square70x70logo" content="/favicons/mstile-70x70.png" />
<meta name="msapplication-square150x150logo" content="/favicons/mstile-150x150.png" />
<meta name="msapplication-wide310x150logo" content="/favicons/mstile-310x150.png" />
<meta name="msapplication-square310x310logo" content="/favicons/mstile-310x310.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.Title}}</title>
<meta name="description" content="{{.Description}}">
<meta property="og:title" content="{{.Title}}">
<meta property="og:description" content="{{.Description}}">
<meta property="og:url" content="{{.OGURL}}">
<meta property="og:type" content="website">
</head>
<body class="text-txt-primary bg-level-0">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,49 +0,0 @@
{
"name": "@probo/trust",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 5175",
"build": "tsc -b && vite build",
"check": "tsc --noEmit -p tsconfig.app.json",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@probo/helpers": "1.0.0",
"@probo/hooks": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/routes": "^1.0.0",
"@probo/ui": "1.0.0",
"clsx": "^2.1.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-pdf": "^10.3.0",
"react-relay": "^21.0.1",
"react-router": "^8.1.0",
"relay-runtime": "^21.0.1",
"usehooks-ts": "^3.1.1",
"zod": "^4.4.3"
},
"devDependencies": {
"@babel/core": "^8.0.1",
"@rolldown/plugin-babel": "^0.2.3",
"@tailwindcss/vite": "^4.3.2",
"@types/babel__core": "^7.20.5",
"@types/node": "^26",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"babel-plugin-relay": "^21.0.1",
"graphql": "^17.0.1",
"tailwindcss": "^4.3.1",
"typescript": "~6.0.3",
"vite": "^8.1.2"
},
"volta": {
"node": "24.4.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@@ -1,33 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Toasts } from "@probo/ui";
import { RouterProvider } from "react-router";
import { router } from "./routes";
export function App() {
return (
<>
<RouterProvider router={router} />
<Toasts />
</>
);
}

View File

@@ -1,238 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { UnAuthenticatedError } from "@probo/relay";
import {
Breadcrumb,
Button,
Dialog,
DialogContent,
FrameworkLogo,
IconArrowLink,
IconLock,
IconMedal,
Table,
useToast,
} from "@probo/ui";
import { type PropsWithChildren } from "react";
import { useFragment, useMutation } from "react-relay";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import type { AuditRow_requestAccessMutation } from "./__generated__/AuditRow_requestAccessMutation.graphql";
import type { AuditRowFragment$key } from "./__generated__/AuditRowFragment.graphql";
const requestAccessMutation = graphql`
mutation AuditRow_requestAccessMutation($input: RequestReportAccessInput!) {
requestReportAccess(input: $input) {
audit {
reportFile {
access {
id
status
}
}
}
}
}
`;
const auditRowFragment = graphql`
fragment AuditRowFragment on Audit {
name
reportFile {
id
alias
isUserAuthorized
access {
id
status
}
}
framework {
id
name
lightLogo {
downloadUrl
}
darkLogo {
downloadUrl
}
}
}
`;
export function AuditRow(props: { audit: AuditRowFragment$key }) {
const { __ } = useTranslate();
const { toast } = useToast();
const [searchParams] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const audit = useFragment(auditRowFragment, props.audit);
const reportPath = audit.reportFile?.alias ?? audit.reportFile?.id;
const hasRequested = audit.reportFile?.access?.status === "REQUESTED";
const [requestAccess, isRequestingAccess]
= useMutation<AuditRow_requestAccessMutation>(requestAccessMutation);
const handleRequestAccess = () => {
requestAccess({
variables: {
input: {
reportId: audit.reportFile?.id ?? "",
},
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
},
onError: (error) => {
if (error instanceof UnAuthenticatedError) {
searchParams.set("request-report-id", audit.reportFile?.id ?? "");
const urlSearchParams = new URLSearchParams([[
"continue",
window.location.origin + location.pathname + "?" + searchParams.toString(),
]]);
void navigate(`/connect?${urlSearchParams.toString()}`);
return;
}
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
return (
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconMedal size={16} className="flex-none text-txt-tertiary" />
{audit.name ?? audit.framework.name}
</div>
{audit.reportFile && (
audit.reportFile.isUserAuthorized
? (
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconArrowLink}
to={reportPath ? `/documents/${reportPath}` : undefined}
>
{__("View")}
</Button>
)
: (
<Button
disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
onClick={handleRequestAccess}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
)
)}
</div>
);
}
export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) {
const audit = useFragment(auditRowFragment, props.audit);
return (
<>
<AuditDialog audit={props.audit}>
<button
className="block cursor-pointer aspect-square"
title={`Logo ${audit.framework.name}`}
>
<div className="flex flex-col gap-2 items-center w-19">
<FrameworkLogo
className="size-19"
lightLogoURL={audit.framework.lightLogo?.downloadUrl}
darkLogoURL={audit.framework.darkLogo?.downloadUrl}
name={audit.framework.name}
/>
<div className="txt-primary text-sm max-w-19 overflow-hidden min-w-0 whitespace-nowrap text-ellipsis">
{audit.framework.name}
</div>
</div>
</button>
</AuditDialog>
</>
);
}
function AuditDialog(
props: PropsWithChildren<{ audit: AuditRowFragment$key; logo?: string }>,
) {
const audit = useFragment(auditRowFragment, props.audit);
const location = useLocation();
const { __ } = useTranslate();
const items = [
{
label: __("Certifications"),
to: location.pathname,
},
{
label: audit.framework.name,
to: location.pathname,
},
];
return (
<Dialog
trigger={props.children}
className="max-w-[500px]"
title={<Breadcrumb items={items} />}
>
<DialogContent className="p-4 lg:p-8 space-y-6">
<FrameworkLogo
className="size-24 mx-auto"
lightLogoURL={audit.framework.lightLogo?.downloadUrl}
darkLogoURL={audit.framework.darkLogo?.downloadUrl}
name={audit.framework.name}
/>
<h2 className="text-xl font-semibold mb-1">{audit.framework.name}</h2>
<Table>
<AuditRow audit={props.audit} />
</Table>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,157 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { UnAuthenticatedError } from "@probo/relay";
import {
Button,
IconArrowLink,
IconLock,
IconPageTextLine,
useToast,
} from "@probo/ui";
import { useFragment, useMutation } from "react-relay";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import type { CompliancePortalFileRow_requestAccessMutation } from "./__generated__/CompliancePortalFileRow_requestAccessMutation.graphql";
import type { CompliancePortalFileRowFragment$key } from "./__generated__/CompliancePortalFileRowFragment.graphql";
const requestAccessMutation = graphql`
mutation CompliancePortalFileRow_requestAccessMutation(
$input: RequestCompliancePortalFileAccessInput!
) {
requestCompliancePortalFileAccess(input: $input) {
file {
access {
id
status
}
}
}
}
`;
const compliancePortalFileRowFragment = graphql`
fragment CompliancePortalFileRowFragment on CompliancePortalFile {
id
alias
name
isUserAuthorized
access {
id
status
}
}
`;
export function CompliancePortalFileRow(props: {
file: CompliancePortalFileRowFragment$key;
}) {
const { __ } = useTranslate();
const { toast } = useToast();
const location = useLocation();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const file = useFragment(compliancePortalFileRowFragment, props.file);
const filePath = file.alias ?? file.id;
const hasRequested = file.access?.status === "REQUESTED";
const [requestAccess, isRequestingAccess]
= useMutation<CompliancePortalFileRow_requestAccessMutation>(
requestAccessMutation,
);
const handleRequestAccess = () => {
requestAccess({
variables: {
input: {
compliancePortalFileId: file.id,
},
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
},
onError: (error) => {
if (error instanceof UnAuthenticatedError) {
searchParams.set("request-file-id", file.id);
const urlSearchParams = new URLSearchParams([[
"continue",
window.location.origin + location.pathname + "?" + searchParams.toString(),
]]);
void navigate(`/connect?${urlSearchParams.toString()}`);
return;
}
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
return (
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
{file.name}
</div>
{file.isUserAuthorized
? (
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconArrowLink}
onClick={() => void navigate(`/documents/${filePath}`)}
>
{__("View")}
</Button>
)
: (
<Button
disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
onClick={handleRequestAccess}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
)}
</div>
);
}

View File

@@ -1,107 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import {
FullNameRequiredError,
NDASignatureRequiredError,
UnAuthenticatedError,
} from "@probo/relay";
import { Button, IconChevronLeft, IconPageCross } from "@probo/ui";
import { Link, Navigate, useLocation, useRouteError } from "react-router";
export function DocumentPageErrorBoundary() {
const error = useRouteError();
const location = useLocation();
const { __ } = useTranslate();
const search = new URLSearchParams();
if (location.pathname !== "/" || location.search !== "") {
search.set("continue", window.location.href);
}
const queryString = search.toString();
if (error instanceof UnAuthenticatedError) {
return (
<Navigate
replace
to={{
pathname: "/connect",
search: queryString ? "?" + queryString : "",
}}
/>
);
}
if (error instanceof FullNameRequiredError) {
return (
<Navigate
replace
to={{
pathname: "/full-name",
search: queryString ? "?" + queryString : "",
}}
/>
);
}
if (error instanceof NDASignatureRequiredError) {
return (
<Navigate
replace
to={{
pathname: "/nda",
search: queryString ? "?" + queryString : "",
}}
/>
);
}
return (
<div className="flex flex-col h-screen bg-level-2">
<header className="flex items-center h-12 gap-3 border-b border-border-solid px-4 flex-none bg-level-1">
<Link
to="/documents"
className="size-8 grid place-items-center hover:bg-secondary-hover rounded-sm transition-all"
>
<IconChevronLeft size={16} />
</Link>
</header>
<main className="flex-1 min-h-0 flex items-center justify-center">
<div className="text-center max-w-sm">
<IconPageCross size={32} className="mx-auto text-txt-tertiary mb-4" />
<h2 className="text-lg font-medium mb-2">
{__("Document not found")}
</h2>
<p className="text-sm text-txt-secondary mb-6">
{__("The document you are looking for does not exist or has been removed.")}
</p>
<Button variant="secondary" asChild className="inline-flex">
<Link to="/documents">
{__("Back to documents")}
</Link>
</Button>
</div>
</main>
</div>
);
}

View File

@@ -1,153 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { UnAuthenticatedError } from "@probo/relay";
import {
Button,
IconArrowLink,
IconLock,
IconPageTextLine,
useToast,
} from "@probo/ui";
import { useFragment, useMutation } from "react-relay";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import type { DocumentRow_requestAccessMutation } from "./__generated__/DocumentRow_requestAccessMutation.graphql";
import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql";
const requestAccessMutation = graphql`
mutation DocumentRow_requestAccessMutation(
$input: RequestDocumentAccessInput!
) {
requestDocumentAccess(input: $input) {
document {
access {
id
status
}
}
}
}
`;
const documentRowFragment = graphql`
fragment DocumentRowFragment on Document {
id
alias
title
isUserAuthorized
access {
id
status
}
}
`;
export function DocumentRow(props: { document: DocumentRowFragment$key }) {
const { __ } = useTranslate();
const { toast } = useToast();
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const document = useFragment(documentRowFragment, props.document);
const documentPath = document.alias ?? document.id;
const hasRequested = document.access?.status === "REQUESTED";
const [requestAccess, isRequestingAccess]
= useMutation<DocumentRow_requestAccessMutation>(requestAccessMutation);
const handleRequestAccess = () => {
requestAccess({
variables: {
input: {
documentId: document.id,
},
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
},
onError: (error) => {
if (error instanceof UnAuthenticatedError) {
searchParams.set("request-document-id", document.id);
const urlSearchParams = new URLSearchParams([[
"continue",
window.location.origin + location.pathname + "?" + searchParams.toString(),
]]);
void navigate(`/connect?${urlSearchParams.toString()}`);
return;
}
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
return (
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
{document.title}
</div>
{document.isUserAuthorized
? (
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconArrowLink}
onClick={() => void navigate(`/documents/${documentPath}`)}
>
{__("View")}
</Button>
)
: (
<Button
disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
onClick={handleRequestAccess}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
)}
</div>
);
}

View File

@@ -1,57 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { FrameworkLogo } from "@probo/ui";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { FrameworkBadgeFragment$key } from "./__generated__/FrameworkBadgeFragment.graphql";
const frameworkFragment = graphql`
fragment FrameworkBadgeFragment on Framework {
# eslint-disable-next-line relay/unused-fields
id
name
lightLogo {
downloadUrl
}
darkLogo {
downloadUrl
}
}
`;
export function FrameworkBadge(props: { framework: FrameworkBadgeFragment$key }) {
const framework = useFragment(frameworkFragment, props.framework);
return (
<div className="flex flex-col gap-2 items-center w-19">
<FrameworkLogo
className="size-19"
lightLogoURL={framework.lightLogo?.downloadUrl}
darkLogoURL={framework.darkLogo?.downloadUrl}
name={framework.name}
/>
<div className="txt-primary text-xs max-w-19 min-w-0 text-center">
{framework.name}
</div>
</div>
);
}

View File

@@ -1,365 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { detectSocialName, externalLinkProps, formatError } from "@probo/helpers";
import { useSystemTheme } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { UnAuthenticatedError } from "@probo/relay";
import {
Button,
Card,
IconBell2,
IconBlock,
IconLock,
IconMedal,
SocialIcon,
useToast,
} from "@probo/ui";
import { type PropsWithChildren } from "react";
import { useMutation } from "react-relay";
import { useLocation, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import type { CompliancePortalGraphCurrentQuery$data } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
import type { OrganizationSidebar_requestAllAccessesMutation } from "./__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql";
import type { OrganizationSidebar_subscribeToMailingListMutation } from "./__generated__/OrganizationSidebar_subscribeToMailingListMutation.graphql";
import type { OrganizationSidebar_unsubscribeFromMailingListMutation } from "./__generated__/OrganizationSidebar_unsubscribeFromMailingListMutation.graphql";
import { FrameworkBadge } from "./FrameworkBadge";
const requestAllAccessesMutation = graphql`
mutation OrganizationSidebar_requestAllAccessesMutation {
requestAllAccesses {
compliancePortalAccess {
id
}
}
}
`;
const subscribeToMailingListMutation = graphql`
mutation OrganizationSidebar_subscribeToMailingListMutation {
subscribeToMailingList {
subscription {
id
email
createdAt
updatedAt
}
}
}
`;
const unsubscribeFromMailingListMutation = graphql`
mutation OrganizationSidebar_unsubscribeFromMailingListMutation {
unsubscribeFromMailingList {
deletedMailingListSubscriberId @deleteRecord
}
}
`;
export function OrganizationSidebar({
compliancePortal,
isAuthenticated,
}: {
compliancePortal: CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"];
isAuthenticated: boolean;
}) {
const compliancePortalId = compliancePortal?.id;
const { __ } = useTranslate();
const { toast } = useToast();
const theme = useSystemTheme();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const location = useLocation();
const logoFileUrl = theme === "dark"
? (compliancePortal?.darkLogo?.downloadUrl ?? compliancePortal?.logo?.downloadUrl)
: compliancePortal?.logo?.downloadUrl;
const [requestAllAccesses, isRequestingAccess]
= useMutation<OrganizationSidebar_requestAllAccessesMutation>(
requestAllAccessesMutation,
);
const [subscribeToMailingList, isSubscribing]
= useMutation<OrganizationSidebar_subscribeToMailingListMutation>(
subscribeToMailingListMutation,
);
const [unsubscribeFromMailingList, isUnsubscribing]
= useMutation<OrganizationSidebar_unsubscribeFromMailingListMutation>(
unsubscribeFromMailingListMutation,
);
const handleRequestAllAccesses = () => {
requestAllAccesses({
variables: {},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
window.location.href = location.pathname;
},
onError: (error) => {
if (error instanceof UnAuthenticatedError) {
searchParams.set("request-all", "true");
const urlSearchParams = new URLSearchParams([[
"continue",
window.location.origin + location.pathname + "?" + searchParams.toString(),
]]);
void navigate(`/connect?${urlSearchParams.toString()}`);
return;
}
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
const handleSubscribe = () => {
subscribeToMailingList({
variables: {},
updater: (store, data) => {
const subscription = data?.subscribeToMailingList?.subscription;
if (!subscription?.id || !compliancePortalId) return;
const compliancePortalRecord = store.get(compliancePortalId);
if (!compliancePortalRecord) return;
const subscriptionRecord = store.get(subscription.id);
if (!subscriptionRecord) return;
compliancePortalRecord.setLinkedRecord(subscriptionRecord, "viewerSubscription");
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot subscribe"), errors),
variant: "error",
});
return;
}
toast({
title: __("Subscribed"),
description: __("You will be notified of security updates."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot subscribe"),
variant: "error",
});
},
});
};
const handleUnsubscribe = () => {
unsubscribeFromMailingList({
variables: {},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot unsubscribe"), errors),
variant: "error",
});
return;
}
toast({
title: __("Unsubscribed"),
description: __("You will no longer receive security updates."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot unsubscribe"),
variant: "error",
});
},
});
};
if (!compliancePortal) {
return null;
}
return (
<Card className="p-6 relative overflow-hidden border-b border-border-low isolate">
<div className="h-21 bg-[#044E4114] absolute top-0 left-0 right-0 -z-1"></div>
{logoFileUrl
? (
<img
alt=""
src={logoFileUrl}
className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1"
/>
)
: (
<div className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1" />
)}
<h1 className="text-2xl mt-6">{compliancePortal.entityName}</h1>
<p className="text-sm text-txt-secondary mt-1">
{compliancePortal.description}
</p>
<hr className="my-6 -mx-6 h-px bg-border-low border-none" />
{/* Business information */}
<div className="space-y-4">
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
<IconBlock size={16} />
{__("Business information")}
</h2>
{compliancePortal.websiteUrl && (
<BusinessInfo label={__("Website")}>
<a {...externalLinkProps(compliancePortal.websiteUrl)}>
<span className="text-txt-info hover:underline ">
{new URL(compliancePortal.websiteUrl).host}
</span>
</a>
</BusinessInfo>
)}
{compliancePortal.email && (
<BusinessInfo label={__("Contact")}>
<a href={`mailto:${compliancePortal.email}`}>
<span className="text-txt-info hover:underline ">
{compliancePortal.email}
</span>
</a>
</BusinessInfo>
)}
{compliancePortal.headquarterAddress && (
<BusinessInfo label={__("HQ address")}>
{compliancePortal.headquarterAddress}
</BusinessInfo>
)}
{compliancePortal.customLinks.edges.length > 0 && (
<div className="flex flex-wrap gap-x-4 gap-y-2">
{compliancePortal.customLinks.edges.map(({ node }) => (
<a
key={node.id}
{...externalLinkProps(node.url)}
className="flex items-center gap-1.5 text-sm text-txt-secondary hover:text-txt-primary transition-colors"
>
<SocialIcon socialName={detectSocialName(node.url)} size={14} className="shrink-0" />
<span>{node.name}</span>
</a>
))}
</div>
)}
<hr className="my-6 -mx-6 h-px bg-border-low border-none" />
{/* Certifications */}
{compliancePortal.complianceFrameworks.edges.length > 0 && (
<>
<div className="space-y-4">
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
<IconMedal size={16} />
{__("Frameworks")}
</h2>
<div
className="grid grid-cols-4 gap-4"
style={{
gridTemplateColumns: "repeat(auto-fit, 75px",
}}
>
{compliancePortal.complianceFrameworks.edges.map(edge => (
<FrameworkBadge key={edge.node.id} framework={edge.node.framework} />
))}
</div>
</div>
<hr className="my-6 -mx-6 h-px bg-border-low border-none" />
</>
)}
{/* Actions */}
<div className="space-y-2">
<Button
disabled={isRequestingAccess}
variant="primary"
icon={IconLock}
className="w-full h-10"
onClick={handleRequestAllAccesses}
>
{__("Request access")}
</Button>
{isAuthenticated && (
compliancePortal.viewerSubscription
? (
<Button
disabled={isUnsubscribing}
variant="secondary"
icon={IconBell2}
className="w-full h-10"
onClick={handleUnsubscribe}
>
{__("Unsubscribe from updates")}
</Button>
)
: (
<Button
disabled={isSubscribing}
variant="secondary"
icon={IconBell2}
className="w-full h-10"
onClick={handleSubscribe}
>
{__("Subscribe to updates")}
</Button>
)
)}
</div>
</div>
</Card>
);
}
function BusinessInfo({
children,
label,
}: PropsWithChildren<{ label: string }>) {
return (
<div>
<div className="text-xs text-txt-secondary">{label}</div>
<div className="text-sm">{children}</div>
</div>
);
}

View File

@@ -1,168 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import "react-pdf/dist/Page/TextLayer.css";
import "react-pdf/dist/Page/AnnotationLayer.css";
import { times } from "@probo/helpers";
import {
IconArrowInbox,
IconChevronLeft,
IconChevronRight,
IconPlusLarge,
Spinner,
} from "@probo/ui";
import { IconMinusLarge } from "@probo/ui/src/Atoms/Icons/IconMinusLarge";
import { type ComponentProps, useCallback, useEffect, useRef, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
// Worker for PDF.js
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
const btnClass
= "size-8 grid place-items-center hover:bg-secondary-hover cursor-pointer rounded-sm disabled:opacity-30 transition-all";
export function PDFPreview({ src, name }: { src: string; name?: string }) {
const [numPages, setNumPages] = useState(0);
const [scale, setScale] = useState(1.0);
const [currentPage, setCurrentPage] = useState(1);
const documentRef: ComponentProps<typeof Document>["ref"] = useRef(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const onDocumentLoadSuccess: ComponentProps<
typeof Document
>["onLoadSuccess"] = (document) => {
setNumPages(document.numPages);
setCurrentPage(1);
};
const zoomFactor = (factor: number) => () => {
setScale(scale * factor);
};
const currentPageRef = useRef(currentPage);
useEffect(() => {
currentPageRef.current = currentPage;
}, [currentPage]);
const movePage = useCallback((direction: 1 | -1) => {
const prev = currentPageRef.current;
if (prev === 1 && direction === -1) {
return;
}
const newPage = prev + direction;
const page = documentRef.current?.pages.current[newPage - 1];
if (!page) {
return;
}
page.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "center",
});
setCurrentPage(newPage);
}, []);
const resolveCurrentPage = useCallback(() => {
if (!wrapperRef.current) {
return;
}
const pages = documentRef.current?.pages.current;
if (!pages?.length) {
return;
}
const parentRect = wrapperRef.current.getBoundingClientRect();
const parentMiddleY = parentRect.top + parentRect.height / 2;
for (let i = 0; i < pages.length; i++) {
const childRect = pages[i].getBoundingClientRect();
if (childRect.top <= parentMiddleY && childRect.bottom >= parentMiddleY) {
setCurrentPage(i + 1);
return;
}
}
}, []);
const handleDownload = () => {
const link = document.createElement("a");
link.href = src;
link.download = name || "document.pdf";
link.click();
};
return (
<div className="grid grid-rows-[max-content_1fr] h-full bg-subtle">
{/* Custom Zoom Controls */}
<nav className="flex-none flex items-center gap-2 bg-level-1 py-3 text-sm pl-4 pr-3 text-txt-primary">
<div>{name}</div>
<div className="mx-auto flex gap-1 items-center">
<button
onClick={() => movePage(-1)}
className={btnClass}
disabled={currentPage === 1}
>
<IconChevronLeft size={16} />
</button>
<div>
{currentPage}
{" "}
/
{numPages}
</div>
<button onClick={() => movePage(1)} className={btnClass}>
<IconChevronRight size={16} />
</button>
</div>
<button onClick={zoomFactor(0.8)} className={btnClass}>
<IconMinusLarge size={16} />
</button>
<button onClick={zoomFactor(1.2)} className={btnClass}>
<IconPlusLarge size={16} />
</button>
<button onClick={handleDownload} className={btnClass}>
<IconArrowInbox size={16} />
</button>
</nav>
{/* PDF Document */}
<div
className="overflow-auto scroll-p-6"
onScrollEnd={resolveCurrentPage}
ref={wrapperRef}
>
<Document
file={src}
onLoadSuccess={onDocumentLoadSuccess}
className="flex flex-col gap-4 py-10"
ref={documentRef}
>
{numPages === 0 && <Spinner className="mx-auto" />}
{times(numPages, index => (
<Page
className="w-max h-max mx-auto shadow-mid"
key={index.toString()}
pageNumber={index + 1}
scale={scale} // Apply zoom via scale prop
/>
))}
</Document>
</div>
</div>
);
}

View File

@@ -1,115 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import {
Button,
ErrorDetailMessage,
ErrorDetails,
ErrorLayout,
} from "@probo/ui";
import { useEffect, useRef } from "react";
import { Link, useLocation, useRouteError } from "react-router";
type Props = {
resetErrorBoundary?: () => void;
error?: Error;
};
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
const routeError = useRouteError();
const error = routeError ?? propsError;
const { __ } = useTranslate();
const location = useLocation();
const baseLocation = useRef(location);
const isFullPage = Boolean(routeError ?? propsError);
// Reset error boundary on page change
useEffect(() => {
if (
location.pathname !== baseLocation.current.pathname
&& resetErrorBoundary
) {
resetErrorBoundary();
}
}, [location, resetErrorBoundary]);
const actions = (
<Button asChild>
<Link to="/">{__("Go home")}</Link>
</Button>
);
const layoutProps = {
fullPage: isFullPage,
showLogo: isFullPage,
actions,
};
if (!error) {
return (
<ErrorLayout
{...layoutProps}
title={__("Page not found")}
description={__("The page you are looking for does not exist.")}
/>
);
}
if (error instanceof Error
&& error.message
.toLowerCase()
.match(/(token|expired|invalid|401|unauthorized)/)
) {
const isExpiredToken = error.message.toLowerCase().includes("expired");
const title = isExpiredToken
? __("Expired token")
: __("Invalid Access Link");
const description = isExpiredToken
? __(
"This access link has expired. Compliance page access links are valid for 7 days for security reasons.",
)
: __(
"This access link is not valid. It may have been revoked or the link might be incorrect.",
);
return (
<ErrorLayout
{...layoutProps}
title={title}
description={description}
/>
);
}
return (
<ErrorLayout
{...layoutProps}
title={__("Something went wrong")}
description={__("We hit an unexpected error. Head back home to continue.")}
>
{error instanceof Error && (
<ErrorDetails summary={__("Technical details")}>
<ErrorDetailMessage>{error.message}</ErrorDetailMessage>
</ErrorDetails>
)}
</ErrorLayout>
);
}

View File

@@ -1,75 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { FullNameRequiredError, NDASignatureRequiredError, UnAuthenticatedError } from "@probo/relay";
import { Navigate, useLocation, useRouteError } from "react-router";
import { PageError } from "./PageError";
export function RootErrorBoundary() {
const error = useRouteError();
const location = useLocation();
const search = new URLSearchParams();
if (location.pathname !== "/" || location.search !== "") {
search.set("continue", window.location.href);
}
const queryString = search.toString();
if (error instanceof UnAuthenticatedError) {
return (
<Navigate
replace
to={{
pathname: "/connect",
search: queryString ? "?" + queryString : "",
}}
/>
);
}
if (error instanceof FullNameRequiredError) {
return (
<Navigate
replace
to={{
pathname: "/full-name",
search: queryString ? "?" + queryString : "",
}}
/>
);
}
if (error instanceof NDASignatureRequiredError) {
return (
<Navigate
replace
to={{
pathname: "/nda",
search: queryString ? "?" + queryString : "",
}}
/>
);
}
return <PageError error={error instanceof Error ? error : new Error("unknown error")} />;
}

View File

@@ -1,29 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { PropsWithChildren } from "react";
export function RowHeader({ children }: PropsWithChildren) {
return (
<div className="bg-subtle text-xs font-medium text-txt-tertiary uppercase ">
{children}
</div>
);
}

View File

@@ -1,38 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { clsx } from "clsx";
import type { PropsWithChildren } from "react";
export function Rows({
children,
className,
}: PropsWithChildren<{ className?: string }>) {
return (
<div
className={clsx(
"*:first:rounded-t-lg *:last:rounded-b-lg *:px-6 *:py-3 *:-mt-[1px] *:border *:border-border-solid",
className,
)}
>
{children}
</div>
);
}

View File

@@ -1,45 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { getCompliancePortalUrl } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Skeleton, TabLink, Tabs } from "@probo/ui";
import { TabSkeleton } from "./TabSkeleton";
export function MainSkeleton() {
const { __ } = useTranslate();
return (
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] ">
<Skeleton className="w-full h-300" />
<main>
<Tabs className="mb-8">
<TabLink to={getCompliancePortalUrl("overview")}>{__("Overview")}</TabLink>
<TabLink to={getCompliancePortalUrl("documents")}>{__("Documents")}</TabLink>
<TabLink to={getCompliancePortalUrl("subprocessors")}>
{__("Subprocessors")}
</TabLink>
<TabLink to={getCompliancePortalUrl("updates")}>{__("Updates")}</TabLink>
</Tabs>
<TabSkeleton />
</main>
</div>
);
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Skeleton } from "@probo/ui";
export function TabSkeleton() {
return (
<div className="h-64">
<Skeleton className="h-6 w-[110px] mb-1" />
<Skeleton className="h-6 w-[250px] mb-4" />
<Skeleton className="w-full h-[180px]" />
</div>
);
}

View File

@@ -1,77 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { faviconUrl, getCountryName } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { IconPin } from "@probo/ui";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { SubprocessorRowFragment$key } from "./__generated__/SubprocessorRowFragment.graphql";
const subprocessorRowFragment = graphql`
fragment SubprocessorRowFragment on Subprocessor {
name
description
websiteUrl
countries
}
`;
export function SubprocessorRow(props: { subprocessor: SubprocessorRowFragment$key; hasAnyCountries?: boolean }) {
const subprocessor = useFragment(subprocessorRowFragment, props.subprocessor);
const logo = faviconUrl(subprocessor.websiteUrl);
const { __ } = useTranslate();
return (
<div className="flex text-sm leading-tight gap-6 items-center">
{logo
? (
<img
src={logo}
className="size-8 flex-none rounded-lg"
alt=""
/>
)
: (
<div className="size-8 flex-none rounded-lg" />
)}
<div className="flex flex-col gap-2 flex-1">
<span className="text-sm">{subprocessor.name}</span>
<div className="text-xs text-txt-secondary w-full">{subprocessor.description}</div>
{props.hasAnyCountries
&& (
<div className="text-xs flex gap-1 items-start text-txt-quaternary">
{subprocessor.countries.length > 0 && (
<>
<IconPin size={16} className="flex-none" />
<span>
{subprocessor.countries
.map(country => getCountryName(__, country))
.join(", ")}
</span>
</>
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
export function documentTypeLabel(type: string, __: (s: string) => string) {
switch (type) {
case "POLICY":
return __("Policy");
case "GOVERNANCE":
return __("Governance");
case "PROCEDURE":
return __("Procedure");
case "PLAN":
return __("Plan");
case "REGISTER":
return __("Register");
case "RECORD":
return __("Record");
case "REPORT":
return __("Report");
case "TEMPLATE":
return __("Template");
default:
return __("Other");
}
}

View File

@@ -1,34 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useContext } from "react";
import { CompliancePortalContext } from "#/providers/CompliancePortalProvider";
export function useCompliancePortal(): {
id: string;
entityName: string;
} {
const context = useContext(CompliancePortalContext);
if (!context) {
throw new Error("useCompliancePortal must be used within a CompliancePortalProvider");
}
return context;
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
import { type FieldValues, useForm, type UseFormReturn } from "react-hook-form";
import type { z } from "zod";
export function useFormWithSchema<T extends z.ZodType<FieldValues, FieldValues>>(
schema: T,
options: Parameters<typeof useForm<z.input<T>, unknown, z.output<T>>>[0],
): UseFormReturn<z.input<T>, unknown, z.output<T>> {
return useForm<z.input<T>, unknown, z.output<T>>({
...options,
resolver: standardSchemaResolver(schema),
});
}

View File

@@ -1,83 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useToast } from "@probo/ui";
import { useCallback } from "react";
import { useMutation, type UseMutationConfig } from "react-relay";
import type { GraphQLTaggedNode, MutationParameters } from "relay-runtime";
/**
* A decorated useMutation hook that emits toast notifications on success or error.
*/
export function useMutationWithToasts<T extends MutationParameters>(
query: GraphQLTaggedNode,
baseOptions?: {
onSuccess?: (response: T["response"]) => void;
errorMessage?: string;
},
) {
const [mutate, isLoading] = useMutation<T>(query);
const { toast } = useToast();
const { __ } = useTranslate();
const mutateWithToast = useCallback(
(
queryOptions: UseMutationConfig<T> & {
onSuccess?: (response: T["response"]) => void;
errorMessage?: string;
},
) => {
const options = { ...baseOptions, ...queryOptions };
return new Promise<void>((resolve, reject) =>
mutate({
...queryOptions,
onCompleted: (response, error) => {
options.onCompleted?.(response, error);
if (error) {
toast({
title: __("Error"),
description:
options.errorMessage
?? __("Failed to commit this operation."),
variant: "error",
});
reject(error instanceof Error ? error : new Error(__("Failed to commit this operation.")));
return;
}
options.onSuccess?.(response);
resolve();
},
onError: (error) => {
toast({
title: __("Error"),
description:
options.errorMessage ?? __("Failed to commit this operation."),
variant: "error",
});
reject(error);
},
}),
);
},
[mutate, toast, __, baseOptions],
);
return [mutateWithToast, isLoading] as const;
}

View File

@@ -1,216 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { formatError, type GraphQLError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useToast } from "@probo/ui";
import { useEffect } from "react";
import { useMutation } from "react-relay";
import { useLocation, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import type { useRequestAccessCallback_allMutation } from "./__generated__/useRequestAccessCallback_allMutation.graphql";
import type { useRequestAccessCallback_documentMutation } from "./__generated__/useRequestAccessCallback_documentMutation.graphql";
import type { useRequestAccessCallback_fileMutation } from "./__generated__/useRequestAccessCallback_fileMutation.graphql";
import type { useRequestAccessCallback_reportMutation } from "./__generated__/useRequestAccessCallback_reportMutation.graphql";
const documentMutation = graphql`
mutation useRequestAccessCallback_documentMutation(
$input: RequestDocumentAccessInput!
) {
requestDocumentAccess(input: $input) {
document {
access {
id
status
}
}
}
}
`;
const reportMutation = graphql`
mutation useRequestAccessCallback_reportMutation(
$input: RequestReportAccessInput!
) {
requestReportAccess(input: $input) {
audit {
reportFile {
access {
id
status
}
}
}
}
}
`;
const fileMutation = graphql`
mutation useRequestAccessCallback_fileMutation(
$input: RequestCompliancePortalFileAccessInput!
) {
requestCompliancePortalFileAccess(input: $input) {
file {
access {
id
status
}
}
}
}
`;
const allMutation = graphql`
mutation useRequestAccessCallback_allMutation {
requestAllAccesses {
compliancePortalAccess {
id
}
}
}
`;
function errorToastArgs(__: (s: string) => string, error: GraphQLError | GraphQLError[]) {
return {
title: __("Error"),
description: formatError(__("Cannot request access"), error),
variant: "error" as const,
};
}
function successToastArgs(__: (s: string) => string) {
return {
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success" as const,
};
}
export function useRequestAccessCallback() {
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const documentId = searchParams.get("request-document-id");
const reportId = searchParams.get("request-report-id");
const fileId = searchParams.get("request-file-id");
const all = searchParams.get("request-all");
const { __ } = useTranslate();
const { toast } = useToast();
const [requestDocumentAccess] = useMutation<useRequestAccessCallback_documentMutation>(documentMutation);
const [requestReportAccess] = useMutation<useRequestAccessCallback_reportMutation>(reportMutation);
const [requestFileAccess] = useMutation<useRequestAccessCallback_fileMutation>(fileMutation);
const [requestAll] = useMutation<useRequestAccessCallback_allMutation>(allMutation);
useEffect(() => {
if (documentId) {
searchParams.delete("request-document-id");
void requestDocumentAccess({
variables: {
input: { documentId },
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast(errorToastArgs(__, errors));
return;
}
toast(successToastArgs(__));
},
onError: (error) => {
toast(errorToastArgs(__, error));
},
});
setSearchParams(searchParams);
} else if (reportId) {
searchParams.delete("request-report-id");
void requestReportAccess({
variables: {
input: { reportId },
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast(errorToastArgs(__, errors));
return;
}
toast(successToastArgs(__));
},
onError: (error) => {
toast(errorToastArgs(__, error));
},
});
setSearchParams(searchParams);
} else if (fileId) {
searchParams.delete("request-file-id");
void requestFileAccess({
variables: {
input: { compliancePortalFileId: fileId },
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast(errorToastArgs(__, errors));
return;
}
toast(successToastArgs(__));
},
onError: (error) => {
toast(errorToastArgs(__, error));
},
});
setSearchParams(searchParams);
} else if (all) {
searchParams.delete("request-all");
void requestAll({
variables: {},
onCompleted: (_, errors) => {
if (errors?.length) {
toast(errorToastArgs(__, errors));
return;
}
toast(successToastArgs(__));
window.location.href = window.location.origin + location.pathname;
},
onError: (error) => {
toast(errorToastArgs(__, error));
},
});
setSearchParams(searchParams);
}
}, [
documentId,
reportId,
fileId,
all,
__,
requestDocumentAccess,
requestReportAccess,
requestFileAccess,
requestAll,
searchParams,
setSearchParams,
toast,
location,
]);
}

View File

@@ -1,53 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useMemo } from "react";
import { useSearchParams } from "react-router";
export function useSafeContinueUrl(): URL {
const [searchParams] = useSearchParams();
const continueUrlParam = searchParams.get("continue");
const fallback = window.location.origin + "/";
const safeContinueUrl = useMemo(() => {
if (continueUrlParam) {
let continueUrl: URL;
try {
continueUrl = new URL(continueUrlParam, window.location.origin);
} catch {
return new URL(fallback, window.location.origin);
}
if (
continueUrl.origin === window.location.origin
&& continueUrl.pathname.startsWith("/")
) {
return new URL(
continueUrl.pathname + continueUrl.search,
window.location.origin,
);
}
return new URL(fallback, window.location.origin);
}
return new URL(fallback, window.location.origin);
}, [continueUrlParam, fallback]);
return safeContinueUrl;
}

View File

@@ -1,10 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap");
@import "tailwindcss";
@import "@probo/ui/src/theme.css";
@import "tw-animate-css";
@source "../../../packages/ui/src";
@source "../../../packages/helpers/src";
.react-pdf__Page__annotations.annotationLayer {
display: none;
}

View File

@@ -1,78 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useFavicon, useSystemTheme } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Logo, TabLink, Tabs } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { OrganizationSidebar } from "#/components/OrganizationSidebar";
import { useRequestAccessCallback } from "#/hooks/useRequestAccessCallback";
import { CompliancePortalProvider } from "#/providers/CompliancePortalProvider";
import type { CompliancePortalGraphCurrentQuery } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
import { currentCompliancePortalGraphQuery } from "#/queries/CompliancePortalGraph";
type Props = {
queryRef: PreloadedQuery<CompliancePortalGraphCurrentQuery>;
};
export function MainLayout(props: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery<CompliancePortalGraphCurrentQuery>(currentCompliancePortalGraphQuery, props.queryRef);
const compliancePortal = data.currentCompliancePortal;
const isAuthenticated = data.viewer != null;
const theme = useSystemTheme();
useFavicon(
theme === "dark"
? (compliancePortal?.darkLogo?.downloadUrl ?? compliancePortal?.logo?.downloadUrl)
: compliancePortal?.logo?.downloadUrl,
);
useRequestAccessCallback();
return (
<CompliancePortalProvider compliancePortal={compliancePortal}>
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] lg:items-start ">
<OrganizationSidebar compliancePortal={compliancePortal} isAuthenticated={isAuthenticated} />
<main>
<Tabs className="mb-8">
<TabLink to="/overview">{__("Overview")}</TabLink>
<TabLink to="/documents">{__("Documents")}</TabLink>
{compliancePortal.subprocessorInfo.totalCount > 0
&& <TabLink to="/subprocessors">{__("Subprocessors")}</TabLink>}
<TabLink to="/updates">{__("Updates")}</TabLink>
</Tabs>
<Outlet context={{ compliancePortal }} />
</main>
</div>
<a
href="https://www.probo.com/"
className="flex gap-2 text-sm font-medium text-txt-tertiary items-center w-max mx-auto my-10"
>
{__("Powered by")}
{" "}
<Logo withPicto className="h-6" />
</a>
</CompliancePortalProvider>
);
}

View File

@@ -1,46 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "react-dom/client";
import "./index.css";
import { App } from "./App";
import { RelayProvider } from "./providers/RelayProviders";
import { TranslatorProvider } from "./providers/TranslatorProvider";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<RelayProvider>
<TranslatorProvider>
<App />
</TranslatorProvider>
</RelayProvider>
</QueryClientProvider>,
);

View File

@@ -1,23 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
declare module "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.149/+esm" {
export * from "pdfjs-dist";
}

View File

@@ -1,502 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { formatError } from "@probo/helpers";
import { useSystemTheme } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { UnAuthenticatedError } from "@probo/relay";
import {
Button,
IconArrowDown,
IconChevronLeft,
IconLock,
Spinner,
useToast,
} from "@probo/ui";
import { useEffect, useState } from "react";
import {
type PreloadedQuery,
useMutation,
usePreloadedQuery,
} from "react-relay";
import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import { PDFPreview } from "#/components/PDFPreview";
import type { DocumentPageExportCompliancePortalFileMutation } from "./__generated__/DocumentPageExportCompliancePortalFileMutation.graphql";
import type { DocumentPageExportDocumentMutation } from "./__generated__/DocumentPageExportDocumentMutation.graphql";
import type { DocumentPageExportReportMutation } from "./__generated__/DocumentPageExportReportMutation.graphql";
import type { DocumentPageQuery as DocumentPageQueryType } from "./__generated__/DocumentPageQuery.graphql";
import type { DocumentPageRequestCompliancePortalFileAccessMutation } from "./__generated__/DocumentPageRequestCompliancePortalFileAccessMutation.graphql";
import type { DocumentPageRequestDocumentAccessMutation } from "./__generated__/DocumentPageRequestDocumentAccessMutation.graphql";
import type { DocumentPageRequestReportAccessMutation } from "./__generated__/DocumentPageRequestReportAccessMutation.graphql";
export const documentPageQuery = graphql`
query DocumentPageQuery($alias: String!) {
currentCompliancePortal {
logo {
downloadUrl
}
darkLogo {
downloadUrl
}
}
aliasedNode(alias: $alias) @required(action: THROW) {
__typename
... on Document {
id
title
isUserAuthorized
access {
id
status
}
}
... on CompliancePortalFile {
id
name
isUserAuthorized
access {
id
status
}
}
... on AuditReport {
id
fileName
isUserAuthorized
access {
id
status
}
}
}
}
`;
const exportDocumentMutation = graphql`
mutation DocumentPageExportDocumentMutation(
$input: ExportDocumentPDFInput!
) {
exportDocumentPDF(input: $input) {
data
}
}
`;
const exportCompliancePortalFileMutation = graphql`
mutation DocumentPageExportCompliancePortalFileMutation(
$input: ExportCompliancePortalFileInput!
) {
exportCompliancePortalFile(input: $input) {
data
}
}
`;
const exportReportMutation = graphql`
mutation DocumentPageExportReportMutation(
$input: ExportReportPDFInput!
) {
exportReportPDF(input: $input) {
data
}
}
`;
const requestDocumentAccessMutation = graphql`
mutation DocumentPageRequestDocumentAccessMutation(
$input: RequestDocumentAccessInput!
) {
requestDocumentAccess(input: $input) {
document {
access {
id
status
}
}
}
}
`;
const requestCompliancePortalFileAccessMutation = graphql`
mutation DocumentPageRequestCompliancePortalFileAccessMutation(
$input: RequestCompliancePortalFileAccessInput!
) {
requestCompliancePortalFileAccess(input: $input) {
file {
access {
id
status
}
}
}
}
`;
const requestReportAccessMutation = graphql`
mutation DocumentPageRequestReportAccessMutation(
$input: RequestReportAccessInput!
) {
requestReportAccess(input: $input) {
audit {
reportFile {
access {
id
status
}
}
}
}
}
`;
type Props = {
queryRef: PreloadedQuery<DocumentPageQueryType>;
};
function isPdfDataUri(dataUri: string): boolean {
return dataUri.startsWith("data:application/pdf;");
}
function extractBase64Data(dataUri: string): string {
const commaIndex = dataUri.indexOf(",");
if (commaIndex === -1) return dataUri;
return dataUri.substring(commaIndex + 1);
}
function extractMimeType(dataUri: string): string {
const match = dataUri.match(/^data:([^;]+);/);
return match?.[1] ?? "application/octet-stream";
}
function getNodeTitle(node: DocumentPageQueryType["response"]["aliasedNode"]): string | undefined {
switch (node.__typename) {
case "Document":
return node.title;
case "CompliancePortalFile":
return node.name;
case "AuditReport":
return node.fileName;
default:
return undefined;
}
}
function getNodeId(node: DocumentPageQueryType["response"]["aliasedNode"]): string | undefined {
switch (node.__typename) {
case "Document":
case "CompliancePortalFile":
case "AuditReport":
return node.id;
default:
return undefined;
}
}
export function DocumentPage({ queryRef }: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const theme = useSystemTheme();
const navigate = useNavigate();
const location = useLocation();
const [searchParams] = useSearchParams();
const [pdfData, setPdfData] = useState<string | null>(null);
const [fileData, setFileData] = useState<string | null>(null);
const [exportError, setExportError] = useState<string | null>(null);
const data = usePreloadedQuery<DocumentPageQueryType>(documentPageQuery, queryRef);
const compliancePortal = data.currentCompliancePortal;
const node = data.aliasedNode;
if (
node.__typename !== "Document"
&& node.__typename !== "CompliancePortalFile"
&& node.__typename !== "AuditReport"
) {
throw new Error(`Unexpected node type: ${node.__typename}`);
}
const nodeTitle = getNodeTitle(node);
const nodeId = getNodeId(node);
const logoFileUrl = theme === "dark"
? (compliancePortal?.darkLogo?.downloadUrl ?? compliancePortal?.logo?.downloadUrl)
: compliancePortal?.logo?.downloadUrl;
const [exportDocument, isExportingDocument]
= useMutation<DocumentPageExportDocumentMutation>(exportDocumentMutation);
const [exportFile, isExportingFile]
= useMutation<DocumentPageExportCompliancePortalFileMutation>(exportCompliancePortalFileMutation);
const [exportReport, isExportingReport]
= useMutation<DocumentPageExportReportMutation>(exportReportMutation);
const [requestAccess, isRequestingAccess]
= useMutation<DocumentPageRequestDocumentAccessMutation>(requestDocumentAccessMutation);
const [requestFileAccess, isRequestingFileAccess]
= useMutation<DocumentPageRequestCompliancePortalFileAccessMutation>(requestCompliancePortalFileAccessMutation);
const [requestReportAccess, isRequestingReportAccess]
= useMutation<DocumentPageRequestReportAccessMutation>(requestReportAccessMutation);
const isExporting = isExportingDocument || isExportingFile || isExportingReport;
const [prevNodeId, setPrevNodeId] = useState(nodeId);
if (prevNodeId !== nodeId) {
setPrevNodeId(nodeId);
setPdfData(null);
setFileData(null);
setExportError(null);
}
useEffect(() => {
if (!node.isUserAuthorized || pdfData || fileData || exportError) return;
const onError = (error: Error) => {
setExportError(error.message ?? __("Cannot export document"));
};
const onCompletedErrors = (errors: readonly { message: string }[] | null | undefined) => {
if (errors?.length) {
setExportError(formatError(__("Cannot export document"), [...errors]));
return true;
}
return false;
};
switch (node.__typename) {
case "Document":
exportDocument({
variables: { input: { documentId: node.id } },
onCompleted: (response, errors) => {
if (onCompletedErrors(errors)) return;
setPdfData(response.exportDocumentPDF.data);
},
onError,
});
break;
case "CompliancePortalFile":
exportFile({
variables: { input: { compliancePortalFileId: node.id } },
onCompleted: (response, errors) => {
if (onCompletedErrors(errors)) return;
if (isPdfDataUri(response.exportCompliancePortalFile.data)) {
setPdfData(response.exportCompliancePortalFile.data);
} else {
setFileData(response.exportCompliancePortalFile.data);
}
},
onError,
});
break;
case "AuditReport":
exportReport({
variables: { input: { reportId: node.id } },
onCompleted: (response, errors) => {
if (onCompletedErrors(errors)) return;
setPdfData(response.exportReportPDF.data);
},
onError,
});
break;
}
}, [node, pdfData, fileData, exportError, exportDocument, exportFile, exportReport, __]);
const handleRequestAccess = () => {
const onError = (error: Error) => {
if (error instanceof UnAuthenticatedError) {
const urlSearchParams = new URLSearchParams([[
"continue",
window.location.origin + location.pathname + "?" + searchParams.toString(),
]]);
void navigate(`/connect?${urlSearchParams.toString()}`);
return;
}
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
};
const onCompleted = (_: unknown, errors: readonly { message: string }[] | null | undefined) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), [...errors]),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
};
switch (node.__typename) {
case "Document":
requestAccess({
variables: { input: { documentId: node.id } },
onCompleted,
onError,
});
break;
case "CompliancePortalFile":
requestFileAccess({
variables: { input: { compliancePortalFileId: node.id } },
onCompleted,
onError,
});
break;
case "AuditReport":
requestReportAccess({
variables: { input: { reportId: node.id } },
onCompleted,
onError,
});
break;
}
};
const isRequesting = isRequestingAccess || isRequestingFileAccess || isRequestingReportAccess;
const hasRequested = node.access?.status === "REQUESTED";
const isPdf = node.__typename === "Document" || node.__typename === "AuditReport" || (node.__typename === "CompliancePortalFile" && pdfData !== null);
const handleDownload = () => {
if (!fileData || !nodeTitle) return;
const base64 = extractBase64Data(fileData);
const mimeType = extractMimeType(fileData);
const byteCharacters = atob(base64);
const byteNumbers = new Uint8Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const blob = new Blob([byteNumbers], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = nodeTitle;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="flex flex-col h-screen bg-level-2">
<header className="flex items-center h-12 gap-3 border-b border-border-solid px-4 flex-none bg-level-1">
<Link
to="/documents"
className="size-8 grid place-items-center hover:bg-secondary-hover rounded-sm transition-all"
>
<IconChevronLeft size={16} />
</Link>
{logoFileUrl && (
<img
alt=""
src={logoFileUrl}
className="h-6 w-auto"
/>
)}
<span className="text-sm font-medium truncate">
{nodeTitle}
</span>
</header>
<main className="flex-1 min-h-0">
{exportError
? (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-sm">
<h2 className="text-lg font-medium mb-2">
{__("Failed to load document")}
</h2>
<p className="text-sm text-txt-secondary">
{exportError}
</p>
</div>
</div>
)
: node.isUserAuthorized
? (
isPdf
? (
isExporting || !pdfData
? (
<div className="flex items-center justify-center h-full">
<Spinner />
</div>
)
: (
<PDFPreview src={pdfData} name={nodeTitle ?? ""} />
)
)
: (
isExporting || !fileData
? (
<div className="flex items-center justify-center h-full">
<Spinner />
</div>
)
: (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-sm">
<h2 className="text-lg font-medium mb-2">
{nodeTitle}
</h2>
<p className="text-sm text-txt-secondary mb-6">
{__("This file cannot be previewed in the browser.")}
</p>
<Button
icon={IconArrowDown}
onClick={handleDownload}
>
{__("Download file")}
</Button>
</div>
</div>
)
)
)
: (
<div className="flex items-center justify-center h-full">
<div className="text-center max-w-sm">
<IconLock size={32} className="mx-auto text-txt-tertiary mb-4" />
<h2 className="text-lg font-medium mb-2">
{nodeTitle}
</h2>
<p className="text-sm text-txt-secondary mb-6">
{__("This document requires access approval before viewing.")}
</p>
<Button
disabled={hasRequested || isRequesting}
icon={IconLock}
onClick={handleRequestAccess}
>
{hasRequested
? __("Access requested")
: __("Request access")}
</Button>
</div>
</div>
)}
</main>
</div>
);
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { useParams } from "react-router";
import { RelayProvider } from "#/providers/RelayProviders";
import type { DocumentPageQuery } from "./__generated__/DocumentPageQuery.graphql";
import { DocumentPage, documentPageQuery } from "./DocumentPage";
function DocumentPageQueryLoader() {
const { documentId } = useParams<{ documentId: string }>();
const [queryRef, loadQuery] = useQueryLoader<DocumentPageQuery>(documentPageQuery);
useEffect(() => {
if (documentId) {
loadQuery({ alias: documentId });
}
}, [documentId, loadQuery]);
if (!queryRef) return null;
return <DocumentPage queryRef={queryRef} />;
}
export default function DocumentPageLoader() {
return (
<RelayProvider>
<DocumentPageQueryLoader />
</RelayProvider>
);
}

View File

@@ -1,78 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { groupBy, objectEntries } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Fragment } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { CompliancePortalFileRow } from "#/components/CompliancePortalFileRow";
import { DocumentRow } from "#/components/DocumentRow";
import { RowHeader } from "#/components/RowHeader";
import { Rows } from "#/components/Rows";
import { documentTypeLabel } from "#/helpers/documents";
import type { CompliancePortalGraphCurrentDocumentsQuery } from "#/queries/__generated__/CompliancePortalGraphCurrentDocumentsQuery.graphql";
import { currentTrustDocumentsQuery } from "#/queries/CompliancePortalGraph";
type Props = {
queryRef: PreloadedQuery<CompliancePortalGraphCurrentDocumentsQuery>;
};
export function DocumentsPage({ queryRef }: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery<CompliancePortalGraphCurrentDocumentsQuery>(
currentTrustDocumentsQuery,
queryRef,
);
const documents
= data.currentCompliancePortal?.documents.edges.map(edge => edge.node) ?? [];
const files
= data.currentCompliancePortal?.compliancePortalFiles.edges.map(edge => edge.node) ?? [];
const documentsPerType = groupBy(documents, document =>
documentTypeLabel(document.documentType, __),
);
const filesPerCategory = groupBy(files, file => file.category);
return (
<div>
<h2 className="font-medium mb-1">{__("Documents")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{__("Security and compliance documentation:")}
</p>
<Rows className="mb-8">
{objectEntries(documentsPerType).map(([label, documents]) => (
<Fragment key={label}>
<RowHeader>{label}</RowHeader>
{documents.map(document => (
<DocumentRow key={document.id} document={document} />
))}
</Fragment>
))}
{objectEntries(filesPerCategory).map(([category, files]) => (
<Fragment key={category}>
<RowHeader>{category}</RowHeader>
{files.map(file => (
<CompliancePortalFileRow key={file.id} file={file} />
))}
</Fragment>
))}
</Rows>
</div>
);
}

View File

@@ -1,321 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Button, Card, IconCircleX, Logo, Spinner } from "@probo/ui";
import { startTransition, useEffect, useRef } from "react";
import {
type PreloadedQuery,
useMutation,
usePreloadedQuery,
useRefetchableFragment,
} from "react-relay";
import { Navigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import { useWindowSize } from "usehooks-ts";
import { PDFPreview } from "#/components/PDFPreview";
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 {
# eslint-disable-next-line relay/unused-fields
id
}
currentCompliancePortal @required(action: THROW) {
entityName
nonDisclosureAgreement {
fileName
fileUrl
viewerSignature {
status
}
}
...NDAPageFragment
}
}
`;
const ndaPageFragment = graphql`
fragment NDAPageFragment on CompliancePortal
@refetchable(queryName: "NDAPageRefetchQuery") {
nonDisclosureAgreement @required(action: THROW) {
viewerSignature {
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
}
}
`;
export function NDAPage(props: {
queryRef: PreloadedQuery<NDAPageQueryType>;
}) {
const { __ } = useTranslate();
const [searchParams] = useSearchParams();
const documentViewedRef = useRef(false);
const { width } = useWindowSize();
const isMobile = width < 1100;
const isDesktop = !isMobile;
const queryData = usePreloadedQuery<NDAPageQueryType>(ndaPageQuery, props.queryRef);
const compliancePortal = queryData.currentCompliancePortal;
const viewer = queryData.viewer;
const [data, refetch] = useRefetchableFragment<NDAPageRefetchQuery, NDAPageFragment$key>(
ndaPageFragment,
compliancePortal,
);
const ndaSignature = data.nonDisclosureAgreement.viewerSignature;
const continueUrlParam = searchParams.get("continue");
let safeContinueUrl: string;
if (continueUrlParam) {
try {
const continueUrl = new URL(continueUrlParam, window.location.origin);
if (continueUrl.origin === window.location.origin && continueUrl.pathname.startsWith("/")) {
safeContinueUrl = window.location.origin + continueUrl.pathname + continueUrl.search;
} else {
safeContinueUrl = window.location.origin;
}
} catch {
safeContinueUrl = window.location.origin;
}
} else {
safeContinueUrl = window.location.origin;
}
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) {
window.location.href = safeContinueUrl;
}
}, [isCompleted, safeContinueUrl]);
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 handleAccept = () => {
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,
},
},
});
},
});
};
const nda = compliancePortal.nonDisclosureAgreement;
if (!viewer) {
return <Navigate to="/connect" replace />;
}
if (!nda || !nda.viewerSignature || nda.viewerSignature.status === "COMPLETED") {
return <Navigate to="/overview" replace />;
}
const consentText = ndaSignature?.consentText;
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.",
),
compliancePortal.entityName,
)}
</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>
)}
<div className="mt-8">
<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="button"
onClick={handleAccept}
className="h-10 w-full mt-4"
disabled={isProcessing}
icon={isAccepting ? Spinner : undefined}
>
{isFailed
? __("Try again")
: __("Review and sign")}
</Button>
)}
</div>
</div>
<a
href="https://www.probo.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>
);
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { RelayProvider } from "#/providers/RelayProviders";
import type { NDAPageQuery } from "./__generated__/NDAPageQuery.graphql";
import { NDAPage, ndaPageQuery } from "./NDAPage";
function NDAPageQueryLoader() {
const [queryRef, loadQuery]
= useQueryLoader<NDAPageQuery>(ndaPageQuery);
useEffect(() => {
if (!queryRef) {
loadQuery({});
}
});
if (!queryRef) return null;
return <NDAPage queryRef={queryRef} />;
}
export default function NDAPageLoader() {
return (
<RelayProvider>
<NDAPageQueryLoader />
</RelayProvider>
);
}

View File

@@ -1,269 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import {
getCompliancePortalUrl,
groupBy,
objectEntries,
sprintf,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Card, IconChevronRight } from "@probo/ui";
import { Fragment } from "react";
import { useFragment } from "react-relay";
import { Link, useOutletContext } from "react-router";
import { graphql } from "relay-runtime";
import { AuditRow } from "#/components/AuditRow";
import { CompliancePortalFileRow } from "#/components/CompliancePortalFileRow";
import { DocumentRow } from "#/components/DocumentRow";
import { RowHeader } from "#/components/RowHeader";
import { Rows } from "#/components/Rows";
import { SubprocessorRow } from "#/components/SubprocessorRow";
import { documentTypeLabel } from "#/helpers/documents";
import type { CompliancePortalGraphCurrentQuery$data } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
import type {
OverviewPageFragment$data,
OverviewPageFragment$key,
} from "./__generated__/OverviewPageFragment.graphql";
const overviewFragment = graphql`
fragment OverviewPageFragment on CompliancePortal {
references(first: 14) {
edges {
node {
id
name
logo {
downloadUrl
}
websiteUrl
}
}
}
subprocessors(first: 3) {
edges {
node {
id
countries
...SubprocessorRowFragment
}
}
}
documents(first: 5) {
edges {
node {
id
documentType
...DocumentRowFragment
}
}
}
compliancePortalFiles(first: 5) {
edges {
node {
id
category
...CompliancePortalFileRowFragment
}
}
}
}
`;
export function OverviewPage() {
const { compliancePortal } = useOutletContext<{
compliancePortal: OverviewPageFragment$key
& CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"];
}>();
const fragment = useFragment(overviewFragment, compliancePortal);
return (
<div>
<References
references={fragment.references.edges.map(edge => edge.node)}
/>
<Documents
audits={compliancePortal.audits.edges}
documents={fragment.documents.edges}
files={fragment.compliancePortalFiles.edges}
url={getCompliancePortalUrl("documents")}
/>
<Subprocessors
title={compliancePortal.entityName}
subprocessors={fragment.subprocessors.edges}
url={getCompliancePortalUrl("subprocessors")}
/>
</div>
);
}
function Documents({
documents,
files,
audits,
url,
}: {
documents: OverviewPageFragment$data["documents"]["edges"];
files: OverviewPageFragment$data["compliancePortalFiles"]["edges"];
audits: NonNullable<
CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"]
>["audits"]["edges"];
url: string;
}) {
const { __ } = useTranslate();
const documentsPerType = groupBy(
documents.map(edge => edge.node),
node => documentTypeLabel(node.documentType, __),
);
const filesPerCategory = groupBy(
files.map(edge => edge.node),
node => node.category,
);
const hasAudits = audits.length > 0;
const hasDocuments = hasAudits || documents.length > 0 || files.length > 0;
if (!hasDocuments) {
return null;
}
return (
<div>
<h2 className="font-medium mb-1">{__("Documents")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{__("Security and compliance documentation:")}
</p>
<Rows className="mb-8">
{audits.length > 0 && (
<>
<RowHeader>{__("Compliance")}</RowHeader>
{audits.map(audit => (
<AuditRow key={audit.node.id} audit={audit.node} />
))}
</>
)}
{objectEntries(documentsPerType).map(([label, documents]) => (
<Fragment key={label}>
<RowHeader>{label}</RowHeader>
{documents.map(document => (
<DocumentRow key={document.id} document={document} />
))}
</Fragment>
))}
{objectEntries(filesPerCategory).map(([category, files]) => (
<Fragment key={category}>
<RowHeader>{category}</RowHeader>
{files.map(file => (
<CompliancePortalFileRow key={file.id} file={file} />
))}
</Fragment>
))}
<Link to={url} className="text-sm font-medium flex gap-2 items-center">
{__("See all documents")}
<IconChevronRight size={16} />
</Link>
</Rows>
</div>
);
}
function Subprocessors({
subprocessors,
url,
title,
}: {
subprocessors: OverviewPageFragment$data["subprocessors"]["edges"];
url: string;
title: string;
}) {
const { __ } = useTranslate();
if (subprocessors.length === 0) {
return null;
}
const hasAnyCountries = subprocessors.some((subprocessor) => {
const subprocessorData = subprocessor.node;
return subprocessorData.countries && subprocessorData.countries.length > 0;
});
return (
<div>
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{sprintf(
__("Third-party subprocessors %s work with:"),
title,
)}
</p>
<Rows className="mb-8 *:py-5">
{subprocessors.map(subprocessor => (
<SubprocessorRow
key={subprocessor.node.id}
subprocessor={subprocessor.node}
hasAnyCountries={hasAnyCountries}
/>
))}
<Link to={url} className="text-sm font-medium flex gap-2 items-center">
{__("See all subprocessors")}
<IconChevronRight size={16} />
</Link>
</Rows>
</div>
);
}
type Reference = {
name: string;
logo: { downloadUrl: string } | null;
websiteUrl: string;
id: string;
};
function References({ references }: { references: Reference[] }) {
const { __ } = useTranslate();
if (references.length === 0) {
return null;
}
return (
<div className="mb-8">
<h2 className="font-medium mb-4">{__("Trusted by")}</h2>
<Card className="grid grid-cols-2 flex-wrap p-6 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7">
{references.map(reference => (
<a
key={reference.id}
href={reference.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col justify-center items-center gap-2"
>
<img
src={reference.logo?.downloadUrl}
alt={reference.name}
className="rounded-2xl size-12 block"
/>
<span className="text-xs text-txt-secondary text-center">{reference.name}</span>
</a>
))}
</Card>
</div>
);
}

View File

@@ -1,58 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Rows } from "#/components/Rows";
import { SubprocessorRow } from "#/components/SubprocessorRow";
import type { CompliancePortalGraphCurrentSubprocessorsQuery } from "#/queries/__generated__/CompliancePortalGraphCurrentSubprocessorsQuery.graphql";
import { currentTrustSubprocessorsQuery } from "#/queries/CompliancePortalGraph";
type Props = {
queryRef: PreloadedQuery<CompliancePortalGraphCurrentSubprocessorsQuery>;
};
export function SubprocessorsPage({ queryRef }: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery<CompliancePortalGraphCurrentSubprocessorsQuery>(currentTrustSubprocessorsQuery, queryRef);
const subprocessors
= data.currentCompliancePortal?.subprocessors.edges.map(edge => edge.node) ?? [];
const hasAnyCountries = subprocessors.some(subprocessor => subprocessor.countries.length > 0);
return (
<div>
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{sprintf(
__("Third-party subprocessors %s work with:"),
data.currentCompliancePortal?.entityName ?? "",
)}
</p>
<Rows>
{subprocessors.map(subprocessor => (
<SubprocessorRow key={subprocessor.id} subprocessor={subprocessor} hasAnyCountries={hasAnyCountries} />
))}
</Rows>
</div>
);
}

View File

@@ -1,141 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { IconChevronDown } from "@probo/ui";
import { useState } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import { Rows } from "#/components/Rows";
import type { UpdatesPageQuery } from "#/pages/__generated__/UpdatesPageQuery.graphql";
export const currentTrustUpdatesQuery = graphql`
query UpdatesPageQuery {
currentCompliancePortal {
id
updates(first: 50) {
edges {
node {
id
title
body
updatedAt
}
}
}
}
}
`;
type Props = {
queryRef: PreloadedQuery<UpdatesPageQuery>;
};
export function UpdatesPage({ queryRef }: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery<UpdatesPageQuery>(
currentTrustUpdatesQuery,
queryRef,
);
const items
= data.currentCompliancePortal?.updates.edges.map(e => e.node) ?? [];
return (
<div>
<h2 className="font-medium mb-1">{__("Updates")}</h2>
{items.length === 0
? (
<Rows>
<div className="text-sm text-txt-tertiary text-center py-5">
{__("No updates have been published yet.")}
</div>
</Rows>
)
: (
<>
<p className="text-sm text-txt-secondary mb-4">
{__("Latest compliance and security updates")}
</p>
<div className="space-y-0">
{items.map(item => (
<UpdateItem key={item.id} item={item} />
))}
</div>
</>
)}
</div>
);
}
type UpdateItemType = {
id: string;
title: string;
body: string;
updatedAt: string;
};
function UpdateItem({ item }: { item: UpdateItemType }) {
const [open, setOpen] = useState(false);
return (
<div className="border border-border-solid -mt-px first:rounded-t-lg last:rounded-b-lg overflow-hidden">
<button
type="button"
onClick={() => setOpen(o => !o)}
className="w-full flex items-center justify-between px-6 py-4 text-left hover:bg-highlight transition-colors group"
>
<div className="flex items-center gap-3 min-w-0">
<span className="text-sm font-medium text-txt-primary truncate">
{item.title}
</span>
</div>
<div className="flex items-center gap-4 flex-none ml-4">
<span className="text-xs text-txt-tertiary hidden sm:block">
{new Date(item.updatedAt).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
<IconChevronDown
size={16}
className={`flex-none text-txt-tertiary transition-transform duration-200 ${open ? "rotate-180" : ""}`}
/>
</div>
</button>
{open && (
<div className="px-6 pb-5 pt-0 border-t border-border-solid bg-highlight/40">
<p className="text-sm text-txt-secondary whitespace-pre-wrap leading-relaxed pt-4">
{item.body}
</p>
<p className="text-xs text-txt-tertiary mt-3 sm:hidden">
{new Date(item.updatedAt).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</p>
</div>
)}
</div>
);
}

View File

@@ -1,72 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useSystemTheme } from "@probo/hooks";
import { Card, Logo } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { graphql } from "relay-runtime";
import type { AuthLayoutQuery } from "./__generated__/AuthLayoutQuery.graphql";
export const authLayoutQuery = graphql`
query AuthLayoutQuery {
currentCompliancePortal @required(action: THROW) {
logo {
downloadUrl
}
darkLogo {
downloadUrl
}
}
}
`;
export function AuthLayout(props: { queryRef: PreloadedQuery<AuthLayoutQuery> }) {
const { queryRef } = props;
const { currentCompliancePortal: compliancePage } = usePreloadedQuery<AuthLayoutQuery>(authLayoutQuery, queryRef);
const theme = useSystemTheme();
const logoFileUrl = theme === "dark"
? compliancePage.darkLogo?.downloadUrl ?? compliancePage.logo?.downloadUrl
: compliancePage.logo?.downloadUrl;
return (
<div className="min-h-screen text-txt-primary bg-level-0 flex flex-col items-center justify-center">
<Card className="w-full max-w-lg px-12 py-8 flex flex-col items-center justify-center">
<div className="w-full flex flex-col items-center justify-center gap-8">
{logoFileUrl
? (
<img
alt=""
src={logoFileUrl}
className="h-20 rounded-2xl"
/>
)
: <Logo withPicto className="w-[110px]" />}
<div className="w-full border-t border-t-border-mid" />
</div>
<Outlet />
</Card>
</div>
);
}

View File

@@ -1,49 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { RelayProvider } from "#/providers/RelayProviders";
import type { AuthLayoutQuery } from "./__generated__/AuthLayoutQuery.graphql";
import { AuthLayout, authLayoutQuery } from "./AuthLayout";
function AuthLayoutQueryLoader() {
const [queryRef, loadQuery] = useQueryLoader<AuthLayoutQuery>(authLayoutQuery);
useEffect(() => {
if (!queryRef) {
return loadQuery({});
}
});
if (!queryRef) return null;
return <AuthLayout queryRef={queryRef} />;
}
export default function AuthLayoutLoader() {
return (
<RelayProvider>
<AuthLayoutQueryLoader />
</RelayProvider>
);
}

View File

@@ -1,79 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import { useSafeContinueUrl } from "#/hooks/useSafeContinueUrl";
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
export const connectPageQuery = graphql`
query ConnectPageQuery {
currentCompliancePortal @required(action: THROW) {
entityName
}
}
`;
export function ConnectPage(props: {
queryRef: PreloadedQuery<ConnectPageQuery>;
}) {
const { queryRef } = props;
const { __ } = useTranslate();
const safeContinueUrl = useSafeContinueUrl();
const {
currentCompliancePortal: { entityName },
} = usePreloadedQuery<ConnectPageQuery>(connectPageQuery, queryRef);
usePageTitle(__(`Connect to ${entityName}'s Compliance Page`));
const initiateURL = new URL("/initiate", window.location.origin);
initiateURL.searchParams.set("continue", safeContinueUrl.toString());
return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">
{__(`Connect to ${entityName}'s Compliance Page`)}
</h1>
<p className="text-txt-tertiary">
{__(
"Sign in to start requesting access to documents",
)}
</p>
</div>
<Button
className="w-full h-10"
onClick={() => {
window.location.href = initiateURL.toString();
}}
>
{__("Get Access")}
</Button>
</div>
);
}

View File

@@ -1,54 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { RelayProvider } from "#/providers/RelayProviders";
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
import { ConnectPage, connectPageQuery } from "./ConnectPage";
function ConnectPageQueryLoader() {
const [queryRef, loadQuery]
= useQueryLoader<ConnectPageQuery>(connectPageQuery);
useEffect(() => {
if (!queryRef) {
loadQuery({});
}
});
if (!queryRef) return null;
return (
<Suspense>
<ConnectPage queryRef={queryRef} />
</Suspense>
);
}
export default function ConnectPageLoader() {
return (
<RelayProvider>
<ConnectPageQueryLoader />
</RelayProvider>
);
}

View File

@@ -1,154 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { GraphQLError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui";
import {
useMutation,
} from "react-relay";
import { useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import { z } from "zod";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import type { FullNamePageMutation } from "./__generated__/FullNamePageMutation.graphql";
const updateMutation = graphql`
mutation FullNamePageMutation($input: UpdateFullNameInput!) {
updateFullName(input: $input) {
success
}
}
`;
const schema = z.object({
fullName: z.string().min(2),
});
type FormData = z.infer<typeof schema>;
export default function FullNamePage() {
const { __ } = useTranslate();
const { toast } = useToast();
const [searchParams] = useSearchParams();
const continueUrlParam = searchParams.get("continue");
let safeContinueUrl: string;
if (continueUrlParam) {
try {
const continueUrl = new URL(continueUrlParam, window.location.origin);
if (continueUrl.origin === window.location.origin && continueUrl.pathname.startsWith("/")) {
safeContinueUrl = window.location.origin + continueUrl.pathname + continueUrl.search;
} else {
safeContinueUrl = window.location.origin;
}
} catch {
safeContinueUrl = window.location.origin;
}
} else {
safeContinueUrl = window.location.origin;
}
const {
handleSubmit: handleSubmitWrapper,
register,
formState,
} = useFormWithSchema(schema, {
defaultValues: {
fullName: "",
},
});
const [update] = useMutation<FullNamePageMutation>(
updateMutation,
);
const handleSubmit = handleSubmitWrapper(({ fullName }: FormData) => {
update({
variables: {
input: {
fullName,
},
},
onCompleted: (_, errors: GraphQLError[] | null) => {
if (errors) {
for (const err of errors) {
if (err.extensions?.code === "ALREADY_AUTHENTICATED") {
window.location.href = "/";
return;
}
}
toast({
title: __("Error"),
description: __("Cannot send magic link"),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Full name updated!"),
variant: "success",
});
window.location.href = safeContinueUrl;
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message,
variant: "error",
});
},
});
});
return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">
{__("Please set your profile's full name")}
</h1>
</div>
<form onSubmit={e => void handleSubmit(e)} className="space-y-6">
<Field
label={__("Full Name")}
placeholder="John Doe"
{...register("fullName")}
type="text"
required
error={formState.errors.fullName?.message}
/>
<Button
type="submit"
className="w-xs h-10 mx-auto"
disabled={formState.isSubmitting}
>
{__("Continue")}
</Button>
</form>
</div>
);
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button } from "@probo/ui";
import { useNavigate } from "react-router";
export default function MagicLinkAlreadyUsedPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
usePageTitle(__("Link Already Used"));
return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Link Already Used")}</h1>
<p className="text-txt-tertiary">
{__(
"This magic link has already been used. Magic links can only be used once. Please request a new one if you need to sign in again.",
)}
</p>
</div>
<div className="flex justify-center">
<Button
className="w-xs h-10"
onClick={() => void navigate("/connect")}
>
{__("Request a new link")}
</Button>
</div>
</div>
);
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button } from "@probo/ui";
import { useNavigate } from "react-router";
export default function MagicLinkExpiredPage() {
const { __ } = useTranslate();
const navigate = useNavigate();
usePageTitle(__("Link Expired"));
return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Link Expired")}</h1>
<p className="text-txt-tertiary">
{__(
"This magic link has expired. Magic links are only valid for 15 minutes. Please request a new one.",
)}
</p>
</div>
<div className="flex justify-center">
<Button
className="w-xs h-10"
onClick={() => void navigate("/connect")}
>
{__("Request a new link")}
</Button>
</div>
</div>
);
}

View File

@@ -1,30 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
export function Divider({ children }: { children: React.ReactNode }) {
return (
<div className="relative my-6 w-full">
<div className="border-t border-border-mid" />
<span className="px-4 text-xs uppercase text-txt-secondary bg-level-0 absolute top-0 left-1/2 -translate-1/2">
{children}
</span>
</div>
);
}

View File

@@ -1,79 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { Button, Google, Microsoft } from "@probo/ui";
import type { ComponentProps } from "react";
import { useFragment } from "react-relay";
import { useSearchParams } from "react-router";
import { graphql } from "relay-runtime";
import { useSafeContinueUrl } from "#/hooks/useSafeContinueUrl";
import type { OIDCButtonFragment$key } from "./__generated__/OIDCButtonFragment.graphql";
const fragment = graphql`
fragment OIDCButtonFragment on OIDCProviderInfo {
name
loginURL
}
`;
const providerIcons: Record<
string,
(props: ComponentProps<"svg">) => React.ReactNode
> = {
google: Google,
microsoft: Microsoft,
};
export function OIDCButton({
providerRef,
}: {
providerRef: OIDCButtonFragment$key;
}) {
const { __ } = useTranslate();
const [searchParams] = useSearchParams();
const safeContinueUrl = useSafeContinueUrl();
const provider = useFragment(fragment, providerRef);
const Icon = providerIcons[provider.name];
const organizationId = searchParams.get("organization-id");
return (
<Button
variant="secondary"
className="w-full h-10"
onClick={() => {
const loginURL = new URL(provider.loginURL, window.location.origin);
loginURL.searchParams.set("continue", safeContinueUrl.toString());
if (organizationId) {
loginURL.searchParams.set("organization_id", organizationId);
}
window.location.href = loginURL.toString();
}}
>
<span className="flex items-center gap-2">
{Icon && <Icon width={18} height={18} />}
{__(`Sign in with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}`)}
</span>
</Button>
);
}

View File

@@ -1,33 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { createContext, useMemo } from "react";
export const AuthContext = createContext({ isAuthenticated: false });
type Props = {
children: React.ReactNode;
isAuthenticated: boolean;
};
export function AuthProvider({ children, isAuthenticated }: Props) {
const value = useMemo(() => ({ isAuthenticated }), [isAuthenticated]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

View File

@@ -1,41 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { createContext, type ReactNode } from "react";
import type { CompliancePortalGraphCurrentQuery$data } from "#/queries/__generated__/CompliancePortalGraphCurrentQuery.graphql";
export const CompliancePortalContext = createContext<
CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"] | null
>(null);
export const CompliancePortalProvider = ({
children,
compliancePortal,
}: {
children: ReactNode;
compliancePortal: CompliancePortalGraphCurrentQuery$data["currentCompliancePortal"];
}) => {
return (
<CompliancePortalContext.Provider value={compliancePortal}>
{children}
</CompliancePortalContext.Provider>
);
};

View File

@@ -1,72 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { makeFetchQuery } from "@probo/relay";
import type { PropsWithChildren } from "react";
import { RelayEnvironmentProvider } from "react-relay";
import {
Environment,
Network,
RecordSource,
Store,
} from "relay-runtime";
export function buildEndpoint(): string {
let host = import.meta.env.VITE_API_URL;
if (!host) {
host = window.location.origin;
}
const formattedHost
= host.startsWith("http://") || host.startsWith("https://")
? host
: `https://${host}`;
const url = new URL(formattedHost);
// Compliance pages are always served at the root of a dedicated host.
url.pathname = "/graphql";
return url.toString();
}
const source = new RecordSource();
const store = new Store(source, {
queryCacheExpirationTime: 1 * 60 * 1000,
gcReleaseBufferSize: 20,
});
export const consoleEnvironment = new Environment({
configName: "compliance-page",
network: Network.create(makeFetchQuery(buildEndpoint())),
store,
});
/**
* Provider for relay with the probo environment
*/
export function RelayProvider({ children }: PropsWithChildren) {
return (
<RelayEnvironmentProvider environment={consoleEnvironment}>
{children}
</RelayEnvironmentProvider>
);
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { PropsWithChildren } from "react";
import { TranslatorProvider as ProboTranslatorProvider } from "../../../../packages/i18n/TranslatorProvider";
// TODO : implement a way to retrieve translations strings
const loader = () => {
return Promise.resolve({} as Record<string, string>);
};
/**
* Provider for the translator
*/
export function TranslatorProvider({ children }: PropsWithChildren) {
return (
<ProboTranslatorProvider lang="en" loader={loader}>
{children}
</ProboTranslatorProvider>
);
}

View File

@@ -1,135 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { graphql } from "relay-runtime";
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
// Queries for custom domain (subdomain) approach
export const currentCompliancePortalGraphQuery = graphql`
query CompliancePortalGraphCurrentQuery {
viewer {
id
}
currentCompliancePortal @required(action: THROW) {
id
slug
entityName
description
websiteUrl
email
headquarterAddress
viewerSubscription {
id
email
createdAt
updatedAt
}
logo {
downloadUrl
}
darkLogo {
downloadUrl
}
nonDisclosureAgreement {
fileName
fileUrl
viewerSignature {
status
}
}
customLinks(first: 20) {
edges {
node {
id
name
url
}
}
}
...OverviewPageFragment
subprocessorInfo: subprocessors(first: 0) {
totalCount
}
audits(first: 50) {
edges {
node {
id
...AuditRowFragment
}
}
}
complianceFrameworks(first: 50) {
edges {
node {
id
framework {
...FrameworkBadgeFragment
}
}
}
}
}
}
`;
export const currentTrustDocumentsQuery = graphql`
query CompliancePortalGraphCurrentDocumentsQuery {
currentCompliancePortal {
id
documents(first: 50) {
edges {
node {
id
documentType
...DocumentRowFragment
}
}
}
compliancePortalFiles(first: 50) {
edges {
node {
id
category
...CompliancePortalFileRowFragment
}
}
}
}
}
`;
export const currentTrustSubprocessorsQuery = graphql`
query CompliancePortalGraphCurrentSubprocessorsQuery {
currentCompliancePortal {
id
entityName
subprocessors(first: 50) {
edges {
node {
id
countries
...SubprocessorRowFragment
}
}
}
}
}
`;

View File

@@ -1,164 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { lazy } from "@probo/react-lazy";
import {
type AppRoute,
loaderFromQueryLoader,
routeFromAppRoute,
withQueryRef,
} from "@probo/routes";
import { Fragment } from "react";
import { loadQuery } from "react-relay";
import { createBrowserRouter, redirect } from "react-router";
import { MainLayout } from "#/layouts/MainLayout";
import { DocumentsPage } from "#/pages/DocumentsPage";
import { OverviewPage } from "#/pages/OverviewPage";
import { SubprocessorsPage } from "#/pages/SubprocessorsPage";
import { currentTrustUpdatesQuery, UpdatesPage } from "#/pages/UpdatesPage";
import {
currentCompliancePortalGraphQuery,
currentTrustDocumentsQuery,
currentTrustSubprocessorsQuery,
} from "#/queries/CompliancePortalGraph";
import { DocumentPageErrorBoundary } from "./components/DocumentPageErrorBoundary";
import { PageError } from "./components/PageError";
import { RootErrorBoundary } from "./components/RootErrorBoundary";
import { MainSkeleton } from "./components/Skeletons/MainSkeleton";
import { TabSkeleton } from "./components/Skeletons/TabSkeleton";
import { consoleEnvironment } from "./providers/RelayProviders";
const routes = [
{
Component: lazy(() => import("#/pages/auth/AuthLayoutLoader")),
children: [
{
path: "/connect",
Component: lazy(() => import("#/pages/auth/ConnectPageLoader")),
},
{
path: "/full-name",
Component: lazy(() => import("#/pages/auth/FullNamePage")),
},
],
},
{
path: "/",
loader: () => {
// eslint-disable-next-line
throw redirect("/overview");
},
Component: Fragment,
ErrorBoundary: RootErrorBoundary,
},
{
path: "/nda",
Component: lazy(() => import("#/pages/NDAPageLoader")),
ErrorBoundary: RootErrorBoundary,
},
// Custom domain routes (subdomain-based)
{
path: "/overview",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
ErrorBoundary: RootErrorBoundary,
children: [
{
path: "",
Fallback: TabSkeleton,
Component: OverviewPage,
},
],
},
{
path: "/documents/:documentId",
Component: lazy(() => import("#/pages/DocumentPageLoader")),
ErrorBoundary: DocumentPageErrorBoundary,
},
{
path: "/documents",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
ErrorBoundary: RootErrorBoundary,
children: [
{
path: "",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {}),
),
Fallback: TabSkeleton,
Component: withQueryRef(DocumentsPage),
},
],
},
{
path: "/subprocessors",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
ErrorBoundary: RootErrorBoundary,
children: [
{
path: "",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustSubprocessorsQuery, {}),
),
Fallback: TabSkeleton,
Component: withQueryRef(SubprocessorsPage),
},
],
},
{
path: "/updates",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentCompliancePortalGraphQuery, {}),
),
Component: withQueryRef(MainLayout),
Fallback: MainSkeleton,
ErrorBoundary: RootErrorBoundary,
children: [
{
path: "",
loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustUpdatesQuery, {}),
),
Fallback: TabSkeleton,
Component: withQueryRef(UpdatesPage),
},
],
},
// Fallback URL to the NotFound Page
{
path: "*",
Component: PageError,
},
] satisfies AppRoute[];
export const router = createBrowserRouter(routes.map(routeFromAppRoute), {});

View File

@@ -1,28 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
export type NodeOf<T>
= NonNullable<T> extends
| { readonly edges: ReadonlyArray<{ readonly node: infer U }> }
| undefined
? U
: never;
export type ItemOf<T> = T extends (infer U)[] ? U : never;

View File

@@ -1,29 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,26 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package truststatics
import "embed"
//go:embed dist
var StaticFiles embed.FS

View File

@@ -1,30 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"paths": {
"#/*": ["./src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"allowJs": true,
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,55 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { fileURLToPath, URL } from "node:url";
import babel from "@rolldown/plugin-babel";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vite.dev/config/
// @vitejs/plugin-react@6 (Vite 8) no longer runs Babel, so the Relay tagged
// template transform is applied via @rolldown/plugin-babel instead.
export default defineConfig({
plugins: [
react(),
babel({ plugins: ["relay"] }),
tailwindcss(),
],
build: {
assetsDir: "assets",
},
base: "./",
server: {
port: 5175,
proxy: {
"^/trust/[^/]+/api": {
target: "http://localhost:8080",
changeOrigin: true,
},
},
},
resolve: {
alias: {
"#": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});

View File

@@ -5,7 +5,7 @@
Template files use the extension pattern `<name>.<output-ext>.tmpl`: Template files use the extension pattern `<name>.<output-ext>.tmpl`:
``` ```
pkg/trust/sitemap.xml.tmpl pkg/complianceportal/visitor/sitemap.xml.tmpl
pkg/server/mailactions/templates/page.html.tmpl pkg/server/mailactions/templates/page.html.tmpl
pkg/cookiebanner/prompts/tracker_identification.txt.tmpl pkg/cookiebanner/prompts/tracker_identification.txt.tmpl
pkg/probo/templates/risk_list.json.tmpl pkg/probo/templates/risk_list.json.tmpl

View File

@@ -17,7 +17,7 @@ Configured in `apps/console/src/environments.ts`. Each has its own store with 1-
## Relay compiler ## Relay compiler
Config lives in `relay.config.json` at the repo root with three projects (`core`, `iam`, `trust`) mapped to different source directories and schemas. Each project uses `schema` pointing to `base.graphql` and `schemaExtensions` pointing to the `graphql/` directory containing the per-entity schema files. Generated files go into `__generated__/` directories. Config lives in `relay.config.json` at the repo root with three projects (`core`, `iam`, `complianceportal`) mapped to different source directories and schemas. Each project uses `schema` pointing to `base.graphql` and `schemaExtensions` pointing to the `graphql/` directory containing the per-entity schema files. Generated files go into `__generated__/` directories.
```sh ```sh
make relay # merge split schemas + clean + compile make relay # merge split schemas + clean + compile

View File

@@ -120,7 +120,7 @@ PROBOD_AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-3
PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \ PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \
PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \ PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \
PROBOD_OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \ PROBOD_OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \
PROBOD_API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174,http://${VM_IP}:5175" \ PROBOD_API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174" \
PROBOD_AWS_ENDPOINT="http://127.0.0.1:8333" \ PROBOD_AWS_ENDPOINT="http://127.0.0.1:8333" \
PROBOD_AWS_ACCESS_KEY_ID="probod" \ PROBOD_AWS_ACCESS_KEY_ID="probod" \
PROBOD_AWS_SECRET_ACCESS_KEY="thisisnotasecret" \ PROBOD_AWS_SECRET_ACCESS_KEY="thisisnotasecret" \

View File

@@ -4,7 +4,7 @@ import { defineConfig, globalIgnores } from "eslint/config";
// Workspaces that are linted by this root config. Each gets the shared rule // Workspaces that are linted by this root config. Each gets the shared rule
// sets below; everything else is ignored so a bare `eslint .` keeps the same // sets below; everything else is ignored so a bare `eslint .` keeps the same
// scope as the previous per-workspace configs. // scope as the previous per-workspace configs.
const appDirs = ["apps/console/**", "apps/trust/**", "apps/compliance-portal/**"]; const appDirs = ["apps/console/**", "apps/compliance-portal/**"];
const reactDirs = [...appDirs, "packages/ui/**", "packages/relay/**", "packages/routes/**"]; const reactDirs = [...appDirs, "packages/ui/**", "packages/relay/**", "packages/routes/**"];
const lintedDirs = [...reactDirs, "packages/eslint-config/**"]; const lintedDirs = [...reactDirs, "packages/eslint-config/**"];
@@ -50,8 +50,8 @@ export default defineConfig([
{ {
// compliance-portal mutates through the awaitable useMutation bound in // compliance-portal mutates through the awaitable useMutation bound in
// #/lib/relay/useMutation (over @probo/relay's createUseMutation), never // #/lib/relay/useMutation (over @probo/relay's createUseMutation), never
// react-relay's useMutation directly. Scoped to this app only: console and // react-relay's useMutation directly. Scoped to this app only: console
// trust still use react-relay's useMutation. // still uses react-relay's useMutation.
files: ["apps/compliance-portal/**"], files: ["apps/compliance-portal/**"],
rules: { rules: {
"no-restricted-imports": [ "no-restricted-imports": [

290
package-lock.json generated
View File

@@ -594,292 +594,6 @@
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
}, },
"apps/trust": {
"name": "@probo/trust",
"version": "0.0.0",
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@probo/helpers": "1.0.0",
"@probo/hooks": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/routes": "^1.0.0",
"@probo/ui": "1.0.0",
"clsx": "^2.1.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-pdf": "^10.3.0",
"react-relay": "^21.0.1",
"react-router": "^8.1.0",
"relay-runtime": "^21.0.1",
"usehooks-ts": "^3.1.1",
"zod": "^4.4.3"
},
"devDependencies": {
"@babel/core": "^8.0.1",
"@rolldown/plugin-babel": "^0.2.3",
"@tailwindcss/vite": "^4.3.2",
"@types/babel__core": "^7.20.5",
"@types/node": "^26",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"babel-plugin-relay": "^21.0.1",
"graphql": "^17.0.1",
"tailwindcss": "^4.3.1",
"typescript": "~6.0.3",
"vite": "^8.1.2"
}
},
"apps/trust/node_modules/@babel/code-frame": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
"integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^8.0.0",
"js-tokens": "^10.0.0"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/compat-data": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz",
"integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/core": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz",
"integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^8.0.0",
"@babel/generator": "^8.0.0",
"@babel/helper-compilation-targets": "^8.0.0",
"@babel/helpers": "^8.0.0",
"@babel/parser": "^8.0.0",
"@babel/template": "^8.0.0",
"@babel/traverse": "^8.0.0",
"@babel/types": "^8.0.0",
"@types/gensync": "^1.0.5",
"convert-source-map": "^2.0.0",
"empathic": "^2.0.1",
"gensync": "^1.0.0-beta.2",
"import-meta-resolve": "^4.2.0",
"json5": "^2.2.3",
"obug": "^2.1.1",
"semver": "^7.7.3"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
"apps/trust/node_modules/@babel/generator": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
"integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^8.0.0",
"@babel/types": "^8.0.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"@types/jsesc": "^2.5.0",
"jsesc": "^3.0.2"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/helper-compilation-targets": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz",
"integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^8.0.0",
"@babel/helper-validator-option": "^8.0.0",
"browserslist": "^4.24.0",
"lru-cache": "^11.0.0",
"semver": "^7.7.3"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/helper-globals": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz",
"integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/helper-string-parser": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
"integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/helper-validator-identifier": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.2.tgz",
"integrity": "sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/helper-validator-option": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz",
"integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/helpers": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz",
"integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^8.0.0",
"@babel/types": "^8.0.0"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/parser": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0.tgz",
"integrity": "sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^8.0.0"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/template": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz",
"integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^8.0.0",
"@babel/parser": "^8.0.0",
"@babel/types": "^8.0.0"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/traverse": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.0.tgz",
"integrity": "sha512-bxTj/W2VclGE6CctlfQOpxg8MPDzXArRqkOBePw8EHfebcjF7fETWSS3BriEECo+UiU/Yblq+xUtSImFu7cTbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^8.0.0",
"@babel/generator": "^8.0.0",
"@babel/helper-globals": "^8.0.0",
"@babel/parser": "^8.0.0",
"@babel/template": "^8.0.0",
"@babel/types": "^8.0.0",
"obug": "^2.1.1"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/@babel/types": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0.tgz",
"integrity": "sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^8.0.0",
"@babel/helper-validator-identifier": "^8.0.0"
},
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"apps/trust/node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
"dev": true,
"license": "MIT"
},
"apps/trust/node_modules/lru-cache": {
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"apps/trust/node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"apps/trust/node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"examples/cookie-banner-react": { "examples/cookie-banner-react": {
"name": "@probo/example-cookie-banner-react", "name": "@probo/example-cookie-banner-react",
"version": "0.0.0", "version": "0.0.0",
@@ -4826,10 +4540,6 @@
"resolved": "packages/skills", "resolved": "packages/skills",
"link": true "link": true
}, },
"node_modules/@probo/trust": {
"resolved": "apps/trust",
"link": true
},
"node_modules/@probo/tsconfig": { "node_modules/@probo/tsconfig": {
"resolved": "packages/tsconfig", "resolved": "packages/tsconfig",
"link": true "link": true

View File

@@ -1,30 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
export function getCompliancePortalUrl(path: string): string {
const currentPath = window.location.pathname;
const trustMatch = currentPath.match(/^\/trust\/([^/]+)/);
if (!trustMatch) {
return `/${path}`;
}
return `../${path}`;
}

View File

@@ -122,7 +122,6 @@ export {
fromMaxAgeSeconds, fromMaxAgeSeconds,
} from "./duration"; } from "./duration";
export { getTrackerTypeBadge, getTrackerSourceBadge } from "./tracker"; export { getTrackerTypeBadge, getTrackerSourceBadge } from "./tracker";
export { getCompliancePortalUrl } from "./compliancePortalUrl";
export { detectSocialName } from "./socialUrl"; export { detectSocialName } from "./socialUrl";
export { formatError, type GraphQLError } from "./error"; export { formatError, type GraphQLError } from "./error";
export { Role, roles, getAssignableRoles } from "./roles"; export { Role, roles, getAssignableRoles } from "./roles";

View File

@@ -7,7 +7,6 @@
"sources": { "sources": {
"apps/console/src/pages/iam": "iam", "apps/console/src/pages/iam": "iam",
"apps/console/src": "core", "apps/console/src": "core",
"apps/trust/src": "trust",
"apps/compliance-portal/src": "complianceportal" "apps/compliance-portal/src": "complianceportal"
}, },
"projects": { "projects": {
@@ -43,16 +42,6 @@
"Map": "Record<string, string>" "Map": "Record<string, string>"
} }
}, },
"trust": {
"schema": "pkg/server/api/complianceportal/v1/schema.graphql",
"language": "typescript",
"noFutureProofEnums": true,
"customScalarTypes": {
"Datetime": "string",
"CursorKey": "string",
"EmailAddr": "string"
}
},
"complianceportal": { "complianceportal": {
"schema": "pkg/server/api/complianceportal/v1/schema.graphql", "schema": "pkg/server/api/complianceportal/v1/schema.graphql",
"language": "typescript", "language": "typescript",