Compare commits

..

2 Commits

Author SHA1 Message Date
roshboi 1b1e3d197b Add "Apply Your License Key" self-hosting guide (#3077)
Adds a dedicated self-hosting page documenting how to apply an
Enterprise license key, and cross-links it from the related docs.

New page: self-hosting/configuration/license.mdx — "Apply Your License
Key"
Set NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY (Docker Compose / docker run /
.env tabs)
2026-07-08 16:11:49 +10:00
Lucas Smith a276e18e1f chore: upgrade deps (#3065) 2026-07-08 16:11:00 +10:00
14 changed files with 3266 additions and 2299 deletions
@@ -76,6 +76,8 @@ The Enterprise Edition is required when you:
4. Restart your Documenso instance
5. Verify the license is active in the **Admin Panel** under the **Stats** section
See [Apply Your License Key](/docs/self-hosting/configuration/license) for the full walkthrough, including how to enable individual features once licensed.
</Accordion>
</Accordions>
@@ -197,7 +199,7 @@ See [Support](/docs/policies/support) for complete support options.
1. Sign the Enterprise license agreement
2. Receive license key and access credentials
3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud
4. Configure Enterprise features with support assistance
4. Apply the key — see [Apply Your License Key](/docs/self-hosting/configuration/license) — and configure Enterprise features with support assistance
</Step>
<Step>
@@ -238,6 +240,7 @@ See [Support](/docs/policies/support) for complete support options.
## Related
- [Apply Your License Key](/docs/self-hosting/configuration/license) - Step-by-step license activation
- [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license
- [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ
- [Support](/docs/policies/support) - Support channels and response times
@@ -443,11 +443,11 @@ Telemetry collects only: app version, installation ID, and node ID. No personal
## Enterprise Features
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance.
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. See [Apply Your License Key](/docs/self-hosting/configuration/license) for step-by-step setup.
| Variable | Description |
| ------------------------------------ | ------------------------------------------------ |
| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features |
| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features — see [Apply Your License Key](/docs/self-hosting/configuration/license) for how to apply it |
| `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing |
| `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret |
| `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification |
@@ -0,0 +1,107 @@
---
title: Apply Your License Key
description: Activate your Enterprise license key to unlock enterprise features on your self-hosted instance.
---
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
A license key activates the Enterprise features available to your self-hosted instance, such as CSC signing, SSO, embed white-labelling, and 21 CFR Part 11 compliance.
<Callout type="info">
The license key applies to your **whole instance**, not an individual user account. There's one
key per deployment.
</Callout>
## Prerequisites
- An active Enterprise license key — contact [sales](https://documen.so/enterprise) to set up an
Enterprise subscription, then copy your key from [license.documenso.com](https://license.documenso.com).
See [Enterprise Edition](/docs/policies/enterprise-edition) for details.
- A running self-hosted Documenso instance that you're able to restart
## Step 1: Set the environment variable
Set `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` to your license key.
<Tabs items={['Docker Compose', 'docker run', '.env']}>
<Tab value="Docker Compose">
Add the variable to your `.env` file (or directly under `environment:` in `compose.yml`):
```bash
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
```
Then apply it:
```bash
docker compose up -d
```
</Tab>
<Tab value="docker run">
```bash
docker run -d \
--name documenso \
-e NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here \
documenso/documenso:latest
```
</Tab>
<Tab value=".env">
If you're running Documenso directly (not in a container), add the variable to your `.env` file:
```bash
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
```
</Tab>
</Tabs>
## Step 2: Restart the instance
The license key is only read once, at process startup. Setting the variable in a running container or shell has no effect until the process restarts.
```bash
# Docker Compose
docker compose restart documenso
# Docker
docker restart documenso
```
On startup, Documenso validates the key against the Documenso license server and caches the result locally for future startups, so a brief license-server outage won't lock you out.
## What the license enables
A valid license doesn't turn every enterprise feature on everywhere — activation depends on the feature:
- **CSC signing** activates instance-wide automatically once the license is active and CSC transport is configured. See [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup.
- **SSO, embed white-labelling, 21 CFR Part 11, and similar** are provisioned per organisation. Follow each feature's own guide to configure it once the license is active.
## Troubleshooting
<Accordions type="multiple">
<Accordion title="Enterprise features are still unavailable after applying the key">
- Confirm the key is present in the environment the running process actually reads — `docker
exec` into the container and check `env | grep LICENSE` if unsure.
- Confirm the instance was fully restarted after the variable was set, not just reloaded.
- Re-copy the key to rule out truncation or accidental whitespace.
</Accordion>
<Accordion title="A specific feature still isn't working">
Instance-wide features (like CSC signing) also need their own configuration — an active license
alone isn't enough. Check that feature's guide to confirm the required settings are in place.
Per-organisation features additionally need to be provisioned for the organisation that's using
them.
</Accordion>
</Accordions>
## See Also
- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference
- [Enterprise Edition](/docs/policies/enterprise-edition) - What's included and how to purchase a license
- [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) - Enable CSC-based signing
@@ -2,6 +2,7 @@
"title": "Configuration",
"pages": [
"environment",
"license",
"database",
"email",
"storage",
@@ -49,7 +49,7 @@ The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL`
### Enterprise Edition license
CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`).
CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). See [Apply Your License Key](/docs/self-hosting/configuration/license) to activate one.
</Step>
<Step>
@@ -141,7 +141,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison.
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. Already have a key? See [Apply Your License Key](/docs/self-hosting/configuration/license).
---
@@ -1,20 +1,48 @@
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import type { TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { DOCUMENT_AUDIT_LOG_TYPE, type TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { formatDocumentAuditLogAction } from '@documenso/lib/utils/document-audit-logs';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import type { DateTimeFormatOptions } from 'luxon';
import { DateTime } from 'luxon';
import { match, P } from 'ts-pattern';
import { UAParser } from 'ua-parser-js';
export type AuditLogDataTableProps = {
logs: TDocumentAuditLog[];
};
const dateFormat: DateTimeFormatOptions = {
...DateTime.DATETIME_SHORT,
hourCycle: 'h12',
};
/**
* Get the color indicator for the audit log type
*/
const getAuditLogIndicatorColor = (type: string) =>
match(type)
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => 'bg-green-500')
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => 'bg-red-500')
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, () => 'bg-orange-500')
.with(
P.union(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED, DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED),
() => 'bg-blue-500',
)
.otherwise(() => 'bg-muted');
/**
* DO NOT USE TRANS. YOU MUST USE _ FOR THIS FILE AND ALL CHILDREN COMPONENTS.
*/
const formatUserAgent = (userAgent: string, userAgentInfo: UAParser.IResult) => {
const formatUserAgent = (userAgent: string | null | undefined, userAgentInfo: UAParser.IResult) => {
if (!userAgent) {
return msg`N/A`;
}
const browser = userAgentInfo.browser.name;
const version = userAgentInfo.browser.version;
const os = userAgentInfo.os.name;
@@ -35,45 +63,74 @@ export const InternalAuditLogTable = ({ logs }: AuditLogDataTableProps) => {
const parser = new UAParser();
return (
<div className="divide-y divide-border">
<div className="space-y-4">
{logs.map((log, index) => {
parser.setUA(log.userAgent || '');
const formattedAction = formatDocumentAuditLogAction(i18n, log);
const userAgentInfo = parser.getResult();
const createdAt = DateTime.fromJSDate(log.createdAt).setLocale(APP_I18N_OPTIONS.defaultLocale);
const metaSegments = [
log.email,
log.ipAddress,
log.userAgent ? _(formatUserAgent(log.userAgent, userAgentInfo)) : null,
].filter((segment): segment is string => Boolean(segment));
return (
<div
<Card
key={index}
className="flex gap-5 py-2.5"
// Add top margin for the first card to ensure it's not cut off from the 2nd page onwards
className={`border shadow-sm ${index > 0 ? 'print:mt-8' : ''}`}
style={{
pageBreakInside: 'avoid',
breakInside: 'avoid',
}}
>
<div className="w-[5.5rem] shrink-0">
<div className="text-foreground text-xs">{createdAt.toFormat('yyyy-MM-dd')}</div>
<CardContent className="p-4">
{/* Header Section with indicator, event type, and timestamp */}
<div className="mb-3 flex items-start justify-between">
<div className="flex items-baseline gap-3">
<div className={cn(`h-2 w-2 rounded-full`, getAuditLogIndicatorColor(log.type))} />
<div className="mt-0.5 text-[0.6875rem] text-muted-foreground">{createdAt.toFormat('hh:mm:ss a')}</div>
</div>
<div>
<div className="font-medium text-muted-foreground text-sm uppercase tracking-wide print:text-[8pt]">
{log.type.replace(/_/g, ' ')}
</div>
<div className="min-w-0 flex-1">
<div className="text-pretty text-foreground text-sm print:text-xs">{formattedAction.description}</div>
{metaSegments.length > 0 && (
<div className="mt-1 break-words text-muted-foreground text-xs print:text-[0.6875rem]">
{metaSegments.join(' · ')}
<div className="font-medium text-foreground text-sm print:text-[8pt]">
{formattedAction.description}
</div>
</div>
</div>
)}
</div>
</div>
<div className="text-muted-foreground text-sm print:text-[8pt]">
{DateTime.fromJSDate(log.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toLocaleString(dateFormat)}
</div>
</div>
<hr className="my-4" />
{/* Details Section - Two column layout */}
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-xs print:text-[6pt]">
<div>
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">{_(msg`User`)}</div>
<div className="mt-1 font-mono text-foreground">{log.email || 'N/A'}</div>
</div>
<div className="text-right">
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
{_(msg`IP Address`)}
</div>
<div className="mt-1 font-mono text-foreground">{log.ipAddress || 'N/A'}</div>
</div>
<div className="col-span-2">
<div className="font-medium text-muted-foreground/70 uppercase tracking-wide">
{_(msg`User Agent`)}
</div>
<div className="mt-1 text-foreground">{_(formatUserAgent(log.userAgent, userAgentInfo))}</div>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
@@ -7,6 +7,7 @@ import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { getTranslations } from '@documenso/lib/utils/i18n';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { EnvelopeType } from '@prisma/client';
@@ -105,86 +106,82 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {
return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<header>
<h1 className="font-semibold text-lg tracking-tight">{_(msg`Audit Log`)}</h1>
<div className="mb-6 border-b pb-4">
<h1 className="font-semibold text-xl">{_(msg`Audit Log`)}</h1>
</div>
<p className="mt-1 text-pretty text-muted-foreground text-sm">{document.title}</p>
</header>
<Card>
<CardContent className="grid grid-cols-2 gap-4 p-6 text-sm print:text-xs">
<p>
<span className="font-medium">{_(msg`Envelope ID`)}</span>
<dl className="mt-6 grid grid-cols-2 gap-x-8 gap-y-5">
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Envelope ID`)}</dt>
<span className="mt-1 block break-words">{document.envelopeId}</span>
</p>
<dd className="mt-1 break-all text-foreground text-sm print:text-xs">{document.envelopeId}</dd>
</div>
<p>
<span className="font-medium">{_(msg`Enclosed Document`)}</span>
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Owner`)}</dt>
<span className="mt-1 block break-words">{document.title}</span>
</p>
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">
{document.user.name} ({document.user.email})
</dd>
</div>
<p>
<span className="font-medium">{_(msg`Status`)}</span>
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Status`)}</dt>
<span className="mt-1 block">
{_(document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description).toUpperCase()}
</span>
</p>
<dd className="mt-1 text-foreground text-sm print:text-xs">
{_(document.deletedAt ? msg`Deleted` : DOCUMENT_STATUS[document.status].description)}
</dd>
</div>
<p>
<span className="font-medium">{_(msg`Owner`)}</span>
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Time Zone`)}</dt>
<span className="mt-1 block break-words">
{document.user.name} ({document.user.email})
</span>
</p>
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">
{document.documentMeta?.timezone ?? 'N/A'}
</dd>
</div>
<p>
<span className="font-medium">{_(msg`Created At`)}</span>
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Created At`)}</dt>
<span className="mt-1 block">
{DateTime.fromJSDate(document.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
</span>
</p>
<dd className="mt-1 text-foreground text-sm print:text-xs">
{DateTime.fromJSDate(document.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
</dd>
</div>
<p>
<span className="font-medium">{_(msg`Last Updated`)}</span>
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Last Updated`)}</dt>
<span className="mt-1 block">
{DateTime.fromJSDate(document.updatedAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
</span>
</p>
<dd className="mt-1 text-foreground text-sm print:text-xs">
{DateTime.fromJSDate(document.updatedAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
</dd>
</div>
<p>
<span className="font-medium">{_(msg`Time Zone`)}</span>
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Enclosed Document`)}</dt>
<span className="mt-1 block break-words">{document.documentMeta?.timezone ?? 'N/A'}</span>
</p>
<dd className="mt-1 break-words text-foreground text-sm print:text-xs">{document.title}</dd>
</div>
<div>
<p className="font-medium">{_(msg`Recipients`)}</p>
<div>
<dt className="font-medium text-muted-foreground text-xs">{_(msg`Recipients`)}</dt>
<dd className="mt-1 text-foreground text-sm print:text-xs">
<ul className="space-y-0.5">
<ul className="mt-1 list-inside list-disc">
{document.recipients.map((recipient) => (
<li key={recipient.id} className="break-words">
{recipient.name} ({recipient.email}) ·{' '}
<li key={recipient.id}>
<span className="text-muted-foreground">
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</span>
[{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}]
</span>{' '}
{recipient.name} ({recipient.email})
</li>
))}
</ul>
</dd>
</div>
</dl>
</div>
</CardContent>
</Card>
<div className="mt-8">
<InternalAuditLogTable logs={auditLogs} />
@@ -9,8 +9,11 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { getTranslations } from '@documenso/lib/utils/i18n';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@documenso/ui/primitives/table';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType, FieldType, SigningStatus } from '@prisma/client';
import { DateTime } from 'luxon';
import { redirect } from 'react-router';
@@ -200,178 +203,165 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
return (
<div className="print-provider pointer-events-none mx-auto max-w-screen-md">
<header>
<h1 className="font-semibold text-lg tracking-tight">{_(msg`Signing Certificate`)}</h1>
<div className="flex items-center">
<h1 className="my-8 font-bold text-2xl">{_(msg`Signing Certificate`)}</h1>
</div>
<p className="mt-1 text-pretty text-muted-foreground text-sm">{document.title}</p>
</header>
<Card>
<CardContent className="p-0">
<Table overflowHidden>
<TableHeader>
<TableRow>
<TableHead>{_(msg`Signer Events`)}</TableHead>
<TableHead>{_(msg`Signature`)}</TableHead>
<TableHead>{_(msg`Details`)}</TableHead>
{/* <TableHead>Security</TableHead> */}
</TableRow>
</TableHeader>
<table className="mt-6 w-full">
<thead>
<tr className="border-border border-b">
<th className="w-[30%] pr-4 pb-2 text-left font-medium text-muted-foreground text-xs">
{_(msg`Signer Events`)}
</th>
<TableBody className="print:text-xs">
{document.recipients.map((recipient, i) => {
const logs = getRecipientAuditLogs(recipient.id);
const signature = getRecipientSignatureField(recipient.id);
<th className="w-[30%] pr-4 pb-2 text-left font-medium text-muted-foreground text-xs">
{_(msg`Signature`)}
</th>
return (
<TableRow key={i} className="print:break-inside-avoid">
<TableCell truncate={false} className="w-[min-content] max-w-[220px] align-top">
<div className="hyphens-auto break-words font-medium">{recipient.name}</div>
<div className="break-all">{recipient.email}</div>
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</p>
<th className="pb-2 text-left font-medium text-muted-foreground text-xs">{_(msg`Details`)}</th>
</tr>
</thead>
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Authentication Level`)}:</span>{' '}
<span className="block">{getAuthenticationLevel(recipient.id)}</span>
</p>
</TableCell>
<tbody className="divide-y divide-border">
{document.recipients.map((recipient, i) => {
const logs = getRecipientAuditLogs(recipient.id);
const signature = getRecipientSignatureField(recipient.id);
const isRejected = Boolean(logs.DOCUMENT_RECIPIENT_REJECTED[0]);
<TableCell truncate={false} className="w-[min-content] align-top">
{signature ? (
<>
<div
className="inline-block rounded-lg p-1"
style={{
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
}}
>
{signature.signature?.signatureImageAsBase64 && (
<img
src={`${signature.signature?.signatureImageAsBase64}`}
alt="Signature"
className="max-h-12 max-w-full"
/>
)}
return (
<tr key={i} className="align-top print:break-inside-avoid">
<td className="py-3 pr-4">
<div className="hyphens-auto break-words font-medium text-sm print:text-xs">{recipient.name}</div>
{signature.signature?.typedSignature && (
<p className="text-center font-signature text-sm">
{signature.signature?.typedSignature}
</p>
)}
</div>
<div className="break-all text-muted-foreground text-xs">{recipient.email}</div>
<div className="mt-0.5 text-muted-foreground text-xs">
{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</div>
<div className="mt-2.5">
<div className="font-medium text-muted-foreground text-xs">{_(msg`Authentication Level`)}</div>
<div className="mt-0.5 text-foreground text-sm print:text-xs">
{getAuthenticationLevel(recipient.id)}
</div>
</div>
</td>
<td className="py-3 pr-4">
{signature ? (
<div className="space-y-2.5">
{!isRejected && (
<div
className="inline-block rounded-lg p-1"
style={{
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
}}
>
{signature.signature?.signatureImageAsBase64 && (
<img
src={`${signature.signature?.signatureImageAsBase64}`}
alt="Signature"
className="max-h-12 max-w-full"
/>
)}
{signature.signature?.typedSignature && (
<p className="text-center font-signature text-sm">{signature.signature?.typedSignature}</p>
)}
</div>
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Signature ID`)}:</span>{' '}
<span className="block font-mono uppercase">{signature.secondaryId}</span>
</p>
</>
) : (
<p className="text-muted-foreground">
<Trans>N/A</Trans>
</p>
)}
<div>
<div className="font-medium text-muted-foreground text-xs">{_(msg`Signature ID`)}</div>
<p className="mt-2 text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`IP Address`)}:</span>{' '}
<span className="inline-block">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
</span>
</p>
<div className="mt-0.5 break-all font-mono text-xs uppercase">{signature.secondaryId}</div>
<p className="mt-1 text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Device`)}:</span>{' '}
<span className="inline-block">
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
</span>
</p>
</TableCell>
<TableCell truncate={false} className="w-[min-content] align-top">
<div className="space-y-1">
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Sent`)}:</span>{' '}
<span className="inline-block">
{logs.EMAIL_SENT[0]
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: logs.DOCUMENT_SENT[0]
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: _(msg`Unknown`)}
</span>
</p>
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Viewed`)}:</span>{' '}
<span className="inline-block">
{logs.DOCUMENT_OPENED[0]
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: _(msg`Unknown`)}
</span>
</p>
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Rejected`)}:</span>{' '}
<span className="inline-block">
{logs.DOCUMENT_RECIPIENT_REJECTED[0]
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_REJECTED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: _(msg`Unknown`)}
</span>
</p>
) : (
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Signed`)}:</span>{' '}
<span className="inline-block">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: _(msg`Unknown`)}
</span>
</p>
)}
<p className="text-muted-foreground text-sm print:text-xs">
<span className="font-medium">{_(msg`Reason`)}:</span>{' '}
<span className="inline-block">
{recipient.signingStatus === SigningStatus.REJECTED
? recipient.rejectionReason
: _(
isOwner(recipient.email)
? FRIENDLY_SIGNING_REASONS['__OWNER__']
: FRIENDLY_SIGNING_REASONS[recipient.role],
)}
</span>
</p>
</div>
</div>
) : (
<p className="text-muted-foreground text-sm print:text-xs">{_(msg`N/A`)}</p>
)}
<div className="mt-2.5">
<div className="font-medium text-muted-foreground text-xs">{_(msg`IP Address`)}</div>
<div className="mt-0.5 text-foreground text-sm print:text-xs">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.ipAddress ?? _(msg`Unknown`)}
</div>
</div>
<div className="mt-2.5">
<div className="font-medium text-muted-foreground text-xs">{_(msg`Device`)}</div>
<div className="mt-0.5 text-foreground text-sm print:text-xs">
{getDevice(logs.DOCUMENT_RECIPIENT_COMPLETED[0]?.userAgent)}
</div>
</div>
</td>
<td className="py-3">
<div className="space-y-2.5">
<div>
<div className="font-medium text-muted-foreground text-xs">{_(msg`Sent`)}</div>
<div className="mt-0.5 text-foreground text-sm print:text-xs">
{logs.EMAIL_SENT[0]
? DateTime.fromJSDate(logs.EMAIL_SENT[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: logs.DOCUMENT_SENT[0]
? DateTime.fromJSDate(logs.DOCUMENT_SENT[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: _(msg`Unknown`)}
</div>
</div>
<div>
<div className="font-medium text-muted-foreground text-xs">{_(msg`Viewed`)}</div>
<div className="mt-0.5 text-foreground text-sm print:text-xs">
{logs.DOCUMENT_OPENED[0]
? DateTime.fromJSDate(logs.DOCUMENT_OPENED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: _(msg`Unknown`)}
</div>
</div>
{logs.DOCUMENT_RECIPIENT_REJECTED[0] ? (
<div>
<div className="font-medium text-red-600 text-xs">{_(msg`Rejected`)}</div>
<div className="mt-0.5 text-red-600 text-sm print:text-xs">
{DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_REJECTED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')}
</div>
</div>
) : (
<div>
<div className="font-medium text-muted-foreground text-xs">{_(msg`Signed`)}</div>
<div className="mt-0.5 text-foreground text-sm print:text-xs">
{logs.DOCUMENT_RECIPIENT_COMPLETED[0]
? DateTime.fromJSDate(logs.DOCUMENT_RECIPIENT_COMPLETED[0].createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)')
: _(msg`Unknown`)}
</div>
</div>
)}
<div>
<div className="font-medium text-muted-foreground text-xs">{_(msg`Reason`)}</div>
<div className="mt-0.5 text-foreground text-sm print:text-xs">
{recipient.signingStatus === SigningStatus.REJECTED
? recipient.rejectionReason
: _(
isOwner(recipient.email)
? FRIENDLY_SIGNING_REASONS['__OWNER__']
: FRIENDLY_SIGNING_REASONS[recipient.role],
)}
</div>
</div>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
{!hidePoweredBy && (
<div className="my-8 flex-row-reverse space-y-4">
@@ -387,7 +377,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
</div>
<div className="flex items-end justify-end gap-x-4">
<p className="flex-shrink-0 font-medium text-muted-foreground text-xs">
<p className="flex-shrink-0 font-medium text-sm print:text-xs">
{_(msg`Signing certificate provided by`)}:
</p>
<BrandingLogo className="max-h-6 print:max-h-4" />
+4 -4
View File
@@ -36,8 +36,8 @@
"@lingui/react": "^5.6.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@react-router/node": "^7.12.0",
"@react-router/serve": "^7.12.0",
"@react-router/node": "^7.18.1",
"@react-router/serve": "^7.18.1",
"@simplewebauthn/browser": "^13.2.2",
"@simplewebauthn/server": "^13.2.2",
"@tanstack/react-query": "5.90.10",
@@ -81,8 +81,8 @@
"@babel/preset-typescript": "^7.28.5",
"@lingui/babel-plugin-lingui-macro": "^5.6.0",
"@lingui/vite-plugin": "^5.6.0",
"@react-router/dev": "^7.12.0",
"@react-router/remix-routes-option-adapter": "^7.12.0",
"@react-router/dev": "^7.18.1",
"@react-router/remix-routes-option-adapter": "^7.18.1",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^28.0.9",
"@rollup/plugin-json": "^6.1.0",
+2048 -1264
View File
File diff suppressed because it is too large Load Diff
@@ -137,7 +137,6 @@ export const generateCertificatePdf = async (options: GenerateCertificatePdfOpti
}),
envelopeOwner,
envelopeId: envelope.id,
envelopeTitle: envelope.title,
qrToken: envelope.qrToken,
hidePoweredBy: organisationClaim.flags.hidePoweredBy ?? false,
pageWidth,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff