From 538b180824dc4c3bbd3b9cb70662a01a69a64ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sat, 27 Jun 2026 17:46:38 +0200 Subject: [PATCH] feat(dpa): self-serve Data Processing Agreement generator (#22243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What A single, region-aware DPA that serves all customers, generated automatically from the customer's deployment. Two layers: 1. **Click-through DPA** — recorded at signup (acceptance = execution), resolving merge fields from the deployment region. Cloud only. 2. **In-app signed-PDF generator** — Settings → Legal → Generate DPA: preview the agreement, enter legal entity + authorized signatory, download a PDF pre-signed by Twenty, and store the executed copy against the workspace with its template version + timestamp. Deep-linkable at `/dpa` (login-gated) for `twenty.com/dpa`. ## How it resolves A typed variable matrix (`dpa-region-config.constant.ts`) maps the deployment region to the contracting Processor entity and terms: - **EU (default)** → Twenty.com SAS, hosting EU/Frankfurt, governing law France, SCC section dormant. - **US (custom)** → Twenty, Inc., hosting US, SCC section active. Region is a deployment-wide setting (`DPA_DEPLOYMENT_REGION`, default EU) behind a `DpaRegionService` seam so it can later become per-workspace without touching callers. The legal text is verbatim from the template (generated into `dpa-template.constant.ts` directly from the source `.docx`); only the 6 merge fields are filled and the SCC sections (7.2–7.5) stay in the document for every region per the spec — only field values branch. Sub-processors are deferred to trust.twenty.com (not enumerated). Billing stays decoupled (Twenty, Inc. remains merchant of record regardless of Processor). ## UI Standard list + create-page pattern (mirrors API keys / webhooks): a list of executed copies (with re-download) — or the agreement preview when none exists — and a top-right blue **Generate DPA** CTA opening a standard create page. The "Legal" item is intentionally **not** in the settings menu; the page is reached via the `/dpa` deep link. ## Notable implementation details - **PDF** is rendered server-side with `@react-pdf/renderer`. The built-in standard-14 fonts only encode ASCII and crash on the template's curly quotes / em–en dashes / accented Latin, so Liberation Sans (OFL) is **subset to a Latin glyph set and embedded as base64 data: URLs** — no font files to ship or resolve at runtime (works in dev, prod-Docker and CI). - New `core.dpaAgreement` table via a fast instance command (FK hash reproduced to match TypeORM). - Self-hosted deployments (billing disabled) skip click-through recording and stamp a prominent "not a valid agreement" banner on the preview and PDF. ## Tests - Unit: resolver (per-region entity/law/SCC state, EU default, no unresolved `{{ }}`, SCC sections present in both regions, self-hosted notice) and HTML renderer. - Integration (`test/integration/graphql/suites/dpa`): preview has no unresolved fields; `generateSignedDpa` renders + persists + returns a downloadable PDF (asserted with accented input to guard the font regression); list re-download. ## ⚠ Needs legal input before go-live (marked `TODO_CONFIRM` in `dpa-region-config.constant.ts`) - Registered-office addresses for Twenty.com SAS and Twenty, Inc. - US deployment governing law (the template only specifies France). - DPO name and the Twenty pre-signed authorized signatory name/title. ## Out of scope (flagged per spec) Intra-group legal agreement and any Stripe/billing-entity changes. A future e-sign provider would plug in at `DpaService.generateSignedDpa` + the signatory input. > Draft until the integration test passes in CI and the legal `TODO_CONFIRM` values are supplied. https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a --- _Generated by [Claude Code](https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a)_ Review in cubic --- .../workflows/ci-dpa-subprocessors-sync.yaml | 79 +++ .../src/metadata/generated/schema.graphql | 1 + .../src/metadata/generated/schema.ts | 5 +- packages/twenty-front/codegen-metadata.cjs | 3 + .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 74 +++ .../modules/app/components/SettingsRoutes.tsx | 17 + .../modules/app/hooks/useCreateAppRouter.tsx | 13 +- .../legal/components/DpaDocumentPreview.tsx | 86 +++ .../settings/legal/components/DpaNotice.tsx | 18 + .../components/SettingsDpaAgreementRow.tsx | 57 ++ .../components/SettingsDpaAgreementsTable.tsx | 75 +++ .../graphql/mutations/generateSignedDpa.ts | 22 + .../legal/graphql/queries/getDpaAgreements.ts | 20 + .../legal/graphql/queries/getDpaPreview.ts | 21 + .../src/modules/settings/legal/types/Dpa.ts | 39 ++ .../pages/settings/legal/SettingsLegalDpa.tsx | 102 +++ .../settings/legal/SettingsLegalDpaNew.tsx | 183 ++++++ .../src/testing/constants/UntestedAppPaths.ts | 3 + packages/twenty-server/nest-cli.json | 4 + packages/twenty-server/package.json | 1 + .../scripts/dpa-sync-subprocessors.ts | 115 ++++ ...0020000-create-dpa-agreement-core-table.ts | 55 ++ .../instance-commands.constant.ts | 2 + .../auth/services/sign-in-up.service.ts | 29 + .../engine/core-modules/core-engine.module.ts | 2 + .../dpa/__tests__/subprocessors-json.spec.ts | 32 + .../dpa/config/dpa-region-config.constant.ts | 44 ++ .../dpa-template-version.constant.ts | 8 + .../dpa/constants/dpa-template.constant.ts | 596 ++++++++++++++++++ .../dpa/constants/oneleet-trust.constant.ts | 2 + .../dpa/constants/subprocessors.json | 109 ++++ .../src/engine/core-modules/dpa/dpa.module.ts | 23 + .../engine/core-modules/dpa/dpa.resolver.ts | 68 ++ .../dpa/dtos/dpa-document-block.dto.ts | 20 + .../core-modules/dpa/dtos/dpa-document.dto.ts | 31 + .../dpa/dtos/generate-signed-dpa.input.ts | 24 + .../dpa/dtos/generate-signed-dpa.result.ts | 12 + .../dpa/entities/dpa-agreement.entity.ts | 94 +++ .../dpa/enums/dpa-agreement-type.enum.ts | 6 + .../dpa/enums/dpa-document-block-kind.enum.ts | 5 + .../core-modules/dpa/enums/dpa-region.enum.ts | 5 + .../core-modules/dpa/pdf/dpa-pdf-document.ts | 176 ++++++ .../engine/core-modules/dpa/pdf/fonts/LICENSE | 130 ++++ .../dpa/pdf/fonts/liberation-sans.fonts.ts | 13 + .../dpa/pdf/render-dpa-to-pdf.util.ts | 8 + .../services/__tests__/dpa.service.spec.ts | 67 ++ .../dpa/services/dpa-region.service.ts | 22 + .../core-modules/dpa/services/dpa.service.ts | 180 ++++++ .../core-modules/dpa/types/dpa.types.ts | 70 ++ .../dpa/types/subprocessor.type.ts | 12 + .../__tests__/render-dpa-to-html.util.spec.ts | 138 ++++ .../utils/__tests__/resolve-dpa.util.spec.ts | 268 ++++++++ .../utils/build-dpa-agreement-record.util.ts | 40 ++ .../dpa/utils/render-dpa-to-html.util.ts | 44 ++ .../dpa/utils/resolve-dpa.util.ts | 143 +++++ .../file/guards/file-by-id.guard.ts | 1 + .../file/interfaces/file-folder.interface.ts | 3 + .../twenty-config/config-variables.ts | 17 + .../suites/dpa/dpa.integration-spec.ts | 177 ++++++ packages/twenty-shared/src/types/AppPath.ts | 3 + .../twenty-shared/src/types/FileFolder.ts | 1 + .../twenty-shared/src/types/SettingsPath.ts | 4 + yarn.lock | 1 + 64 files changed, 3621 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci-dpa-subprocessors-sync.yaml create mode 100644 packages/twenty-front/src/modules/settings/legal/components/DpaDocumentPreview.tsx create mode 100644 packages/twenty-front/src/modules/settings/legal/components/DpaNotice.tsx create mode 100644 packages/twenty-front/src/modules/settings/legal/components/SettingsDpaAgreementRow.tsx create mode 100644 packages/twenty-front/src/modules/settings/legal/components/SettingsDpaAgreementsTable.tsx create mode 100644 packages/twenty-front/src/modules/settings/legal/graphql/mutations/generateSignedDpa.ts create mode 100644 packages/twenty-front/src/modules/settings/legal/graphql/queries/getDpaAgreements.ts create mode 100644 packages/twenty-front/src/modules/settings/legal/graphql/queries/getDpaPreview.ts create mode 100644 packages/twenty-front/src/modules/settings/legal/types/Dpa.ts create mode 100644 packages/twenty-front/src/pages/settings/legal/SettingsLegalDpa.tsx create mode 100644 packages/twenty-front/src/pages/settings/legal/SettingsLegalDpaNew.tsx create mode 100644 packages/twenty-server/scripts/dpa-sync-subprocessors.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-17/2-17-instance-command-fast-1801000020000-create-dpa-agreement-core-table.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/__tests__/subprocessors-json.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/config/dpa-region-config.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/constants/dpa-template-version.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/constants/dpa-template.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/constants/oneleet-trust.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/constants/subprocessors.json create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/dpa.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/dpa.resolver.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/dtos/dpa-document-block.dto.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/dtos/dpa-document.dto.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/dtos/generate-signed-dpa.input.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/dtos/generate-signed-dpa.result.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/entities/dpa-agreement.entity.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/enums/dpa-agreement-type.enum.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/enums/dpa-document-block-kind.enum.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/enums/dpa-region.enum.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/pdf/dpa-pdf-document.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/pdf/fonts/LICENSE create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/pdf/fonts/liberation-sans.fonts.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/pdf/render-dpa-to-pdf.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/services/__tests__/dpa.service.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/services/dpa-region.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/services/dpa.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/types/dpa.types.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/types/subprocessor.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/utils/__tests__/render-dpa-to-html.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/utils/__tests__/resolve-dpa.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/utils/build-dpa-agreement-record.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/utils/render-dpa-to-html.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/dpa/utils/resolve-dpa.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/dpa/dpa.integration-spec.ts diff --git a/.github/workflows/ci-dpa-subprocessors-sync.yaml b/.github/workflows/ci-dpa-subprocessors-sync.yaml new file mode 100644 index 0000000000..0eaae2c49b --- /dev/null +++ b/.github/workflows/ci-dpa-subprocessors-sync.yaml @@ -0,0 +1,79 @@ +name: DPA Sub-Processor Sync + +on: + schedule: + - cron: '0 6 * * 1' # Weekly, Monday at 6 AM UTC + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + pull-requests: write + +jobs: + sync-subprocessors: + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + NODE_OPTIONS: '--max-old-space-size=4096' + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: main + + - name: Install dependencies + uses: ./.github/actions/yarn-install + + - name: Build dependencies + run: npx nx build twenty-shared + + - name: Run sub-processor sync + run: npx nx run twenty-server:ts-node-no-deps-transpile-only -- ./scripts/dpa-sync-subprocessors.ts + + - name: Check for changes + id: changes + run: | + if git diff --quiet packages/twenty-server/src/engine/core-modules/dpa/constants/subprocessors.json; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create pull request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: sync DPA sub-processors from trust center' + title: 'chore: sync DPA sub-processors from trust center' + body: | + Automated weekly sync of `subprocessors.json` from Twenty's Trust Center (OneLeet). + + This keeps the DPA's Annex C (the SCC Annex III list of Sub-Processors) in + lockstep with the canonical list at https://trust.twenty.com — the Trust + Center is the single source of truth; this file is generated from it. + + **Please review before merging** — confirm the added/removed Sub-Processors + are expected, and that customers were notified per Section 6.2 where required. + branch: chore/dpa-subprocessors-sync + base: main + labels: automated + delete-branch: true + + - name: Mint twenty-infra dispatch token + id: app-token + if: steps.changes.outputs.changed == 'true' + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.TWENTY_WORKFLOW_DISPATCHER_CLIENT_ID }} + private-key: ${{ secrets.TWENTY_WORKFLOW_DISPATCHER_PRIVATE_KEY }} + owner: twentyhq + repositories: twenty-infra + permission-actions: write + + - name: Trigger automerge + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + gh workflow run automerge-i18n.yaml --repo twentyhq/twenty-infra --ref main diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index 88af092142..1046d09f7c 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -4653,6 +4653,7 @@ enum FileFolder { EmailAttachment AppTarball GeneratedSdkClient + Dpa } type Subscription { diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 82047f3fef..50b8f4570e 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -3001,7 +3001,7 @@ export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update' export type AnalyticsType = 'PAGEVIEW' | 'TRACK' -export type FileFolder = 'ProfilePicture' | 'WorkspaceLogo' | 'Attachment' | 'PersonPicture' | 'CorePicture' | 'File' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient' +export type FileFolder = 'ProfilePicture' | 'WorkspaceLogo' | 'Attachment' | 'PersonPicture' | 'CorePicture' | 'File' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient' | 'Dpa' export interface Subscription { onEventSubscription?: EventSubscription @@ -9346,5 +9346,6 @@ export const enumFileFolder = { Workflow: 'Workflow' as const, EmailAttachment: 'EmailAttachment' as const, AppTarball: 'AppTarball' as const, - GeneratedSdkClient: 'GeneratedSdkClient' as const + GeneratedSdkClient: 'GeneratedSdkClient' as const, + Dpa: 'Dpa' as const } diff --git a/packages/twenty-front/codegen-metadata.cjs b/packages/twenty-front/codegen-metadata.cjs index 3dd5081043..e6dbb6072b 100644 --- a/packages/twenty-front/codegen-metadata.cjs +++ b/packages/twenty-front/codegen-metadata.cjs @@ -17,6 +17,9 @@ module.exports = { './src/modules/settings/**/graphql/**/*.{ts,tsx}', '!./src/modules/settings/admin-panel/**/graphql/**/*.{ts,tsx}', + // DPA operations are served by the core (/graphql) schema via @CoreResolver, + // not the metadata schema, so they must not be validated against /metadata. + '!./src/modules/settings/legal/**/graphql/**/*.{ts,tsx}', './src/modules/logic-functions/graphql/**/*.{ts,tsx}', './src/modules/databases/graphql/**/*.{ts,tsx}', diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index d646d6f8d5..7b481670a2 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1896,6 +1896,7 @@ export enum FileFolder { BuiltLogicFunction = 'BuiltLogicFunction', CorePicture = 'CorePicture', Dependencies = 'Dependencies', + Dpa = 'Dpa', EmailAttachment = 'EmailAttachment', File = 'File', FilesField = 'FilesField', diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 3c1a5a5455..fd5e4e1d51 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -92,6 +92,60 @@ export type DeleteWorkflowVersionStepInput = { workflowVersionId: Scalars['UUID']['input']; }; +export type DpaAgreement = { + __typename?: 'DpaAgreement'; + acceptedAt: Scalars['DateTime']['output']; + acceptedByEmail?: Maybe; + createdAt: Scalars['DateTime']['output']; + customerLegalEntityName?: Maybe; + downloadUrl?: Maybe; + id: Scalars['UUID']['output']; + processorEntity: Scalars['String']['output']; + region: DpaRegion; + signatoryName?: Maybe; + signatoryTitle?: Maybe; + signedFileId?: Maybe; + templateVersion: Scalars['String']['output']; + type: DpaAgreementType; + updatedAt: Scalars['DateTime']['output']; +}; + +export enum DpaAgreementType { + CLICK_THROUGH = 'CLICK_THROUGH', + SIGNED = 'SIGNED' +} + +export type DpaDocument = { + __typename?: 'DpaDocument'; + blocks: Array; + lastUpdatedLabel: Scalars['String']['output']; + notice?: Maybe; + processorEntity: Scalars['String']['output']; + region: DpaRegion; + sccSectionActive: Scalars['Boolean']['output']; + templateVersion: Scalars['String']['output']; + title: Scalars['String']['output']; +}; + +export type DpaDocumentBlock = { + __typename?: 'DpaDocumentBlock'; + kind: DpaDocumentBlockKind; + label?: Maybe; + text: Scalars['String']['output']; + value?: Maybe; +}; + +export enum DpaDocumentBlockKind { + Heading = 'Heading', + Paragraph = 'Paragraph', + SignatureField = 'SignatureField' +} + +export enum DpaRegion { + EU = 'EU', + US = 'US' +} + export type DuplicateWorkflowInput = { /** Workflow ID to duplicate */ workflowIdToDuplicate: Scalars['UUID']['input']; @@ -109,6 +163,18 @@ export enum FilterIs { Null = 'Null' } +export type GenerateSignedDpaInput = { + customerLegalEntityName: Scalars['String']['input']; + signatoryName: Scalars['String']['input']; + signatoryTitle: Scalars['String']['input']; +}; + +export type GenerateSignedDpaResult = { + __typename?: 'GenerateSignedDpaResult'; + agreement: DpaAgreement; + downloadUrl: Scalars['String']['output']; +}; + export type LinkMetadata = { __typename?: 'LinkMetadata'; label: Scalars['String']['output']; @@ -142,6 +208,7 @@ export type Mutation = { dismissReconnectAccountBanner: Scalars['Boolean']['output']; duplicateWorkflow: WorkflowVersionDto; duplicateWorkflowVersionStep: WorkflowVersionStepChanges; + generateSignedDpa: GenerateSignedDpaResult; retryWorkflowRun: WorkflowRun; runWorkflowVersion: RunWorkflowVersion; stopWorkflowRun: WorkflowRun; @@ -208,6 +275,11 @@ export type MutationDuplicateWorkflowVersionStepArgs = { }; +export type MutationGenerateSignedDpaArgs = { + input: GenerateSignedDpaInput; +}; + + export type MutationRetryWorkflowRunArgs = { workflowRunId: Scalars['UUID']['input']; }; @@ -259,6 +331,8 @@ export type ObjectRecordFilterInput = { export type Query = { __typename?: 'Query'; + dpaAgreements: Array; + dpaPreview: DpaDocument; /** @deprecated Use getTimelineCalendarEventsFromObjectRecord instead */ getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromObjectRecord: TimelineCalendarEventsWithTotal; diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 014b6fd0fb..9b85797554 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -114,6 +114,18 @@ const SettingsGeneral = lazy(() => })), ); +const SettingsLegalDpa = lazy(() => + import('~/pages/settings/legal/SettingsLegalDpa').then((module) => ({ + default: module.SettingsLegalDpa, + })), +); + +const SettingsLegalDpaNew = lazy(() => + import('~/pages/settings/legal/SettingsLegalDpaNew').then((module) => ({ + default: module.SettingsLegalDpaNew, + })), +); + const SettingsWorkspaceEmail = lazy(() => import('~/pages/settings/email/SettingsWorkspaceEmail').then((module) => ({ default: module.SettingsWorkspaceEmail, @@ -682,6 +694,11 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( path={SettingsPath.PublicDomain} element={} /> + } /> + } + /> } /> + {/* Deep link for twenty.com/dpa → in-app generator. This route is + inside the authenticated layout, so an unauthenticated hit is + login-gated and returns here after sign-in. */} + + } + /> ( + // tabIndex makes the scrollable region focusable so keyboard-only users can + // scroll the agreement with the arrow keys. + + {document.title} + + Last Updated: {document.lastUpdatedLabel} + + {document.blocks.map((block, index) => { + if (block.kind === 'heading') { + return {block.text}; + } + + if (block.kind === 'signatureField') { + return ( + + {block.label} + {block.value} + + ); + } + + return {block.text}; + })} + +); diff --git a/packages/twenty-front/src/modules/settings/legal/components/DpaNotice.tsx b/packages/twenty-front/src/modules/settings/legal/components/DpaNotice.tsx new file mode 100644 index 0000000000..d7a27ce8c9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/legal/components/DpaNotice.tsx @@ -0,0 +1,18 @@ +import { styled } from '@linaria/react'; +import { Info } from 'twenty-ui/feedback'; + +const StyledFullWidthInfo = styled.div` + & > * { + max-width: 100%; + } +`; + +type DpaNoticeProps = { + text: string; +}; + +export const DpaNotice = ({ text }: DpaNoticeProps) => ( + + + +); diff --git a/packages/twenty-front/src/modules/settings/legal/components/SettingsDpaAgreementRow.tsx b/packages/twenty-front/src/modules/settings/legal/components/SettingsDpaAgreementRow.tsx new file mode 100644 index 0000000000..bb30345b2e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/legal/components/SettingsDpaAgreementRow.tsx @@ -0,0 +1,57 @@ +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react/macro'; +import { Button } from 'twenty-ui/input'; +import { IconDownload } from 'twenty-ui/icon'; + +import { type DpaAgreement } from '@/settings/legal/types/Dpa'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { beautifyExactDateTime } from '~/utils/date-utils'; + +export const DPA_AGREEMENT_ROW_GRID_COLUMNS = '3fr 2fr 2fr 140px'; + +const StyledEllipsisLabel = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +type SettingsDpaAgreementRowProps = { + agreement: DpaAgreement; + onDownload: (agreement: DpaAgreement) => void; +}; + +export const SettingsDpaAgreementRow = ({ + agreement, + onDownload, +}: SettingsDpaAgreementRowProps) => { + const { t } = useLingui(); + + const label = + agreement.type === 'SIGNED' + ? (agreement.customerLegalEntityName ?? t`Signed copy`) + : t`Click-through acceptance`; + + return ( + + + {label} + + {agreement.templateVersion} + {beautifyExactDateTime(agreement.acceptedAt)} + + {agreement.downloadUrl ? ( +