Compare commits

..
164 changed files with 1263 additions and 5473 deletions
@@ -1,61 +0,0 @@
name: 'One-Click Deploy Provider Request'
description: Request a new one-click deployment provider (Railway, Render, etc.) to be added to our README
title: 'One-Click Deploy Provider Request: [Provider Name]'
labels: ['deploy-provider-request']
body:
- type: markdown
attributes:
value: |
Thanks for your interest in adding a one-click deploy option for Documenso!
Each provider we list requires us to create, test, and maintain a deployment template, which is ongoing work on top of everything else. To keep this manageable, we ask that providers (or users) **open an issue instead of a PR** so the community can signal interest.
**How this works:**
- 👍 this issue if you'd like to see Documenso deployable on this provider.
- If community interest is high enough, we'll consider adding it to the README.
- Opening an issue is not a guarantee of inclusion. PRs adding badges without a prior issue and demonstrated interest will be closed.
- type: input
attributes:
label: Provider Name
placeholder: e.g. Railway
validations:
required: true
- type: input
attributes:
label: Provider Website
placeholder: e.g. https://railway.com
validations:
required: true
- type: input
attributes:
label: Deploy/Template URL
description: A link to an existing deployment template or deploy button URL, if one exists.
- type: dropdown
attributes:
label: Who creates and maintains the deployment template?
options:
- The provider
- Me / the community
- Nobody yet
validations:
required: true
- type: textarea
attributes:
label: Testing & Maintenance
description: Has the template been tested against the current Documenso release? How are updates handled when Documenso ships breaking changes (env vars, migrations, Docker changes)?
validations:
required: true
- type: textarea
attributes:
label: Why this provider?
description: Tell us why Documenso users would benefit — existing user base, region coverage, free tier, etc.
validations:
required: true
- type: checkboxes
attributes:
label: Please check the boxes that apply to this request.
options:
- label: I have searched existing issues to make sure this provider has not already been requested.
- label: I understand that inclusion depends on community interest and is not guaranteed.
- label: I understand that PRs adding deploy badges without a prior issue will be closed.
+1 -1
View File
@@ -2,7 +2,7 @@ name: 'Setup node'
inputs:
node_version:
required: false
default: v24.x
default: v22.x
runs:
using: 'composite'
+15 -31
View File
@@ -107,7 +107,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
To run Documenso locally, you will need
- Node.js (v24 or above)
- Node.js (v22 or above)
- Postgres SQL Database
- Docker (optional)
@@ -186,37 +186,21 @@ For full instructions, requirements, and configuration details, see the [Self Ho
### One-Click Deploys
> [!NOTE]
> Want to see another provider listed here? Please [open a provider request](https://github.com/documenso/documenso/issues/new?template=deploy-provider-request.yml) instead of a PR so the community can signal interest. PRs adding deploy badges without a prior issue will be closed.
#### Railway
<table>
<tr>
<td align="center" width="200">
<a href="https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic">
<img src="https://railway.com/button.svg" alt="Deploy on Railway" height="40" />
</a>
</td>
<td align="center" width="200">
<a href="https://render.com/deploy?repo=https://github.com/documenso/documenso">
<img src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" height="40" />
</a>
</td>
<td align="center" width="200">
<a href="https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile">
<img src="https://www.koyeb.com/static/images/deploy/button.svg" alt="Deploy to Koyeb" height="40" />
</a>
</td>
</tr>
<tr>
<td align="center" width="200">
<a href="https://elest.io/open-source/documenso">
<img src="https://elest.io/images/logos/deploy-to-elestio-btn.png" alt="Deploy on Elestio" height="40" />
</a>
</td>
<td align="center" width="200"></td>
<td align="center" width="200"></td>
</tr>
</table>
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
#### Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)
#### Koyeb
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
#### Elestio
[![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/documenso)
## Security
@@ -95,7 +95,7 @@ Documents created with a team token belong to that team:
<Tab value="curl">
```bash
curl -X POST "https://app.documenso.com/api/v2/envelope/create" \
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
-H "Authorization: api_team_xxxxxxxxxxxxxxxx" \
-H "Content-Type: multipart/form-data" \
-F 'payload={
"type": "DOCUMENT",
@@ -157,11 +157,11 @@ Retrieve all documents belonging to the team:
```bash
# List all team documents
curl -X GET "https://app.documenso.com/api/v2/envelope" \
-H "Authorization: api_xxxxxxxxxxxxxxxx"
-H "Authorization: api_team_xxxxxxxxxxxxxxxx"
# Filter by status
curl -X GET "https://app.documenso.com/api/v2/envelope?status=PENDING" \
-H "Authorization: api_xxxxxxxxxxxxxxxx"
-H "Authorization: api_team_xxxxxxxxxxxxxxxx"
````
</Tab>
@@ -191,7 +191,7 @@ Templates created with a team token are shared across the team.
<Tab value="curl">
```bash
curl -X POST "https://app.documenso.com/api/v2/template/create" \
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
-H "Authorization: api_team_xxxxxxxxxxxxxxxx" \
-H "Content-Type: multipart/form-data" \
-F 'payload={
"title": "NDA Template",
@@ -269,7 +269,7 @@ console.log('Created team template:', template.id);
<Tab value="curl">
```bash
curl -X GET "https://app.documenso.com/api/v2/template" \
-H "Authorization: api_xxxxxxxxxxxxxxxx"
-H "Authorization: api_team_xxxxxxxxxxxxxxxx"
````
</Tab>
@@ -102,7 +102,7 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
| Variable | Description | Default |
| ------------------------------------------- | -------------------------------------------------------------- | ------------------------- |
| `PORT` | Port the application listens on | `3000` |
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | Path to signing certificate inside container — set to the volume-mount path (e.g. `/opt/documenso/cert.p12`). Only Docker Compose defaults this; plain `docker run` must set it explicitly | - |
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | Path to signing certificate inside container | `/opt/documenso/cert.p12` |
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - |
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - |
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` |
@@ -136,7 +136,6 @@ docker run -d \
-e NEXT_PUBLIC_WEBAPP_URL="https://sign.example.com" \
-e NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000" \
-e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@db-host:5432/documenso" \
-e NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH="/opt/documenso/cert.p12" \
-e NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" \
-e NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" \
-e NEXT_PRIVATE_SMTP_HOST="smtp.example.com" \
@@ -155,12 +154,6 @@ A signing certificate is required for document signing. You have two options for
- **Volume mount** — mount a `.p12` file from the host into the container at `/opt/documenso/cert.p12` (shown above). This is the simplest approach for small to moderate deployments.
- **Base64-encoded contents** — set `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` with the base64-encoded certificate string. Use this when file mounting is not available (e.g., Railway, Vercel).
<Callout type="warn">
Plain `docker run` deployments must set `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` explicitly. This
prevents production deployments from accidentally using the insecure example certificate.
Docker Compose sets the file path for you.
</Callout>
For production deployments that require Adobe Approved Trust List recognition, consider using a [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) or another external HSM.
<Callout type="warn">
@@ -185,7 +178,6 @@ NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com
NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000
NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@db-host:5432/documenso
NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@db-host:5432/documenso
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
NEXT_PRIVATE_SMTP_HOST=smtp.example.com
@@ -211,12 +203,6 @@ docker run -d \
Documenso provides health check endpoints for monitoring:
<Callout type="info">
If a certificate is mounted but signing fails, ensure `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH`
explicitly points to its path inside the container. Production does not use the development
example certificate as a fallback.
</Callout>
| Endpoint | Purpose |
| ------------------------- | -------------------------------------------------------------- |
| `/api/health` | Checks database connectivity and certificate status |
@@ -14,8 +14,8 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
## Prerequisites
- Node.js 24 or later
- npm 11.17 or later
- Node.js 22 or later
- npm 11 or later
- PostgreSQL 14 or later
- A Linux server (for systemd service setup)
@@ -141,8 +141,8 @@ If building from source (not using Docker images):
| Requirement | Version |
| ----------- | ------- |
| Node.js | 24+ |
| npm | 11.17+ |
| Node.js | 22+ |
| npm | 11+ |
---
@@ -169,7 +169,7 @@ Documenso runs on:
| MySQL/MariaDB | PostgreSQL-specific features required |
| SQLite | Not suitable for production workloads |
| MongoDB | Relational database required |
| Node.js < 24 | Modern JavaScript features required |
| Node.js < 22 | Modern JavaScript features required |
---
@@ -34,7 +34,7 @@ To access the preferences, navigate to either the organisation or teams settings
| **Default Recipients** | Recipients that are automatically added to new documents. Can be overridden per document. |
| **Default Envelope Expiration** | How long recipients have to sign before the signing link expires. See [recipient expiration](/docs/users/documents/advanced/recipient-expiration). |
| **Default Signing Reminders** | When and how often to email recipients who have not yet signed. See [signing reminders](/docs/users/documents/advanced/signing-reminders). |
| **Delegate Document Ownership** | By default, documents created with a team API token are owned by the user who created the token. Enable this setting to let supported API requests assign ownership to another team member. |
| **Delegate Document Ownership** | Allow team API tokens to delegate document ownership to another team member. |
| **AI Features** | Enable AI-powered features such as automatic recipient detection. Only shown if AI features are configured on the instance. |
Document visibility, language, and signature settings can be overridden per document.
+25
View File
@@ -0,0 +1,25 @@
FROM oven/bun:1 AS dependencies-env
COPY . /app
FROM dependencies-env AS development-dependencies-env
COPY ./package.json bun.lockb /app/
WORKDIR /app
RUN bun i --frozen-lockfile
FROM dependencies-env AS production-dependencies-env
COPY ./package.json bun.lockb /app/
WORKDIR /app
RUN bun i --production
FROM dependencies-env AS build-env
COPY ./package.json bun.lockb /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN bun run build
FROM dependencies-env
COPY ./package.json bun.lockb /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["bun", "run", "start"]
+26
View File
@@ -0,0 +1,26 @@
FROM node:20-alpine AS dependencies-env
RUN npm i -g pnpm
COPY . /app
FROM dependencies-env AS development-dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
WORKDIR /app
RUN pnpm i --frozen-lockfile
FROM dependencies-env AS production-dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
WORKDIR /app
RUN pnpm i --prod --frozen-lockfile
FROM dependencies-env AS build-env
COPY ./package.json pnpm-lock.yaml /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN pnpm build
FROM dependencies-env
COPY ./package.json pnpm-lock.yaml /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["pnpm", "start"]
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -72,7 +71,6 @@ export const EnvelopeDistributeDialog = ({
const { toast } = useToast();
const { t, i18n } = useLingui();
const navigate = useNavigate();
const analytics = useAnalytics();
const [isOpen, setIsOpen] = useState(false);
const [isSyncing, setIsSyncing] = useState(false);
@@ -202,12 +200,6 @@ export const EnvelopeDistributeDialog = ({
} catch (err) {
const error = AppError.parseError(err);
analytics.captureException(err, {
source: 'editor',
location: 'distribute_document',
envelopeId: envelope.id,
});
const errorMessage = getDistributeErrorMessage(error.code);
toast({
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { AppError } from '@documenso/lib/errors/app-error';
import type { TEnvelope } from '@documenso/lib/types/envelope';
@@ -52,7 +51,6 @@ export const EnvelopeRedistributeDialog = ({ envelope, envelopeType, trigger }:
const { toast } = useToast();
const { t, i18n } = useLingui();
const analytics = useAnalytics();
const [isOpen, setIsOpen] = useState(false);
@@ -97,13 +95,6 @@ export const EnvelopeRedistributeDialog = ({ envelope, envelopeType, trigger }:
setIsOpen(false);
} catch (err) {
const error = AppError.parseError(err);
analytics.captureException(err, {
source: 'editor',
location: 'redistribute_document',
envelopeId: envelope.id,
});
const errorMessage = getDistributeErrorMessage(error.code);
toast({
@@ -290,11 +290,10 @@ export const EnvelopesBulkDownloadDialog = ({
{isOverDownloadLimit && (
<Alert variant="warning">
<AlertDescription>
<Plural
value={MAX_BULK_DOWNLOAD_ENVELOPES}
one="You can download up to # document at a time. Deselect some documents to continue."
other="You can download up to # documents at a time. Deselect some documents to continue."
/>
<Trans>
You can download up to {MAX_BULK_DOWNLOAD_ENVELOPES} documents at a time. Deselect some documents to
continue.
</Trans>
</AlertDescription>
</Alert>
)}
@@ -275,9 +275,9 @@ export const ManagePublicTemplateDialog = ({
<TableCell>
{selectedTemplateId === row.id ? (
<CheckCircle2Icon className="h-5 w-5 text-neutral-600 dark:text-neutral-200" />
<CheckCircle2Icon className="h-5 w-5 text-foreground" />
) : (
<CircleIcon className="h-5 w-5 text-neutral-300 dark:text-neutral-600" />
<CircleIcon className="h-5 w-5 text-muted-foreground/40" />
)}
</TableCell>
</TableRow>
@@ -1,7 +1,4 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TBulkSendCsvError } from '@documenso/lib/server-only/template/validate-bulk-send-csv';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import {
@@ -20,9 +17,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { File as FileIcon, Upload, X } from 'lucide-react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { z } from 'zod';
import { useCurrentTeam } from '~/providers/team';
@@ -34,8 +29,6 @@ const ZBulkSendFormSchema = z.object({
type TBulkSendFormSchema = z.infer<typeof ZBulkSendFormSchema>;
type TBulkSendValidationError = TBulkSendCsvError | { type: 'UPLOAD_ERROR'; code: string };
export type TemplateBulkSendDialogProps = {
templateId: number;
recipients: Array<{ email: string; name?: string | null }>;
@@ -49,9 +42,6 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
const team = useCurrentTeam();
const [open, setOpen] = useState(false);
const [validationError, setValidationError] = useState<TBulkSendValidationError | null>(null);
const form = useForm<TBulkSendFormSchema>({
resolver: zodResolver(ZBulkSendFormSchema),
defaultValues: {
@@ -61,20 +51,6 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
const { mutateAsync: uploadBulkSend } = trpc.template.uploadBulkSend.useMutation();
const onOpenChange = (value: boolean) => {
if (form.formState.isSubmitting) {
return;
}
setOpen(value);
if (!value) {
setValidationError(null);
form.reset();
}
};
const onDownloadTemplate = () => {
const headers = recipients.flatMap((_, index) => [`recipient_${index + 1}_email`, `recipient_${index + 1}_name`]);
@@ -95,44 +71,36 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
};
const onSubmit = async (values: TBulkSendFormSchema) => {
setValidationError(null);
try {
const csv = await values.file.text();
const result = await uploadBulkSend({
await uploadBulkSend({
templateId,
teamId: team?.id,
csv: csv,
sendImmediately: values.sendImmediately,
});
if (!result.success) {
setValidationError(result.error);
return;
}
toast({
title: _(msg`Success`),
description: _(msg`Your bulk send has been initiated. You will receive an email notification upon completion.`),
});
setOpen(false);
form.reset();
onSuccess?.();
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
setValidationError({ type: 'UPLOAD_ERROR', code: error.code });
toast({
title: _(msg`Error`),
description: _(msg`Failed to upload CSV. Please check the file format and try again.`),
variant: 'destructive',
});
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog>
<DialogTrigger asChild>
{trigger ?? (
<Button variant="outline" className="shrink-0" size="sm">
@@ -206,10 +174,7 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
setValidationError(null);
onChange(file);
}
}}
@@ -230,11 +195,7 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
type="button"
variant="link"
className="p-0 text-destructive text-xs hover:text-destructive"
onClick={() => {
setValidationError(null);
form.resetField('file');
}}
onClick={() => onChange(null)}
disabled={form.formState.isSubmitting}
>
<X className="h-4 w-4" />
@@ -257,67 +218,6 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
)}
/>
{validationError !== null && (
<Alert variant="destructive">
<AlertDescription className="max-h-32 overflow-y-auto">
{match(validationError)
.with({ type: 'PARSE_ERROR' }, () => (
<Trans>The CSV could not be parsed. Please check the file format and try again.</Trans>
))
.with({ type: 'EMPTY' }, () => (
<Trans>
The CSV does not contain any rows. Please add at least one row of recipient details.
</Trans>
))
.with({ type: 'ROW_LIMIT_EXCEEDED' }, ({ rowCount, maxRows }) => (
<Trans>
The CSV contains {rowCount} rows. A maximum of {maxRows} rows is allowed per upload.
</Trans>
))
.with({ type: 'MISSING_COLUMNS' }, ({ missingColumns }) => (
<>
<Trans>
The CSV is missing the following required columns. Please download the template CSV for the
correct format.
</Trans>
<ul className="mt-1 list-inside list-disc">
{missingColumns.map((column) => (
<li key={column} className="font-mono">
{column}
</li>
))}
</ul>
</>
))
.with({ type: 'INVALID_RECIPIENTS' }, ({ rowErrors }) => (
<>
<Trans>The CSV contains invalid recipient emails. Please fix the following rows:</Trans>
<ul className="mt-1 list-inside list-disc">
{rowErrors.map((rowError, index) => (
<li key={index}>
<Trans>
Row {rowError.row}: <span className="font-mono">{rowError.column}</span> must be a valid
email or empty
</Trans>
</li>
))}
</ul>
</>
))
.with({ type: 'UPLOAD_ERROR' }, ({ code }) =>
code === AppErrorCode.LIMIT_EXCEEDED ? (
<Trans>The CSV exceeds the maximum file size.</Trans>
) : (
<Trans>Failed to upload CSV. Please check the file format and try again.</Trans>
),
)
.exhaustive()}
</AlertDescription>
</Alert>
)}
<FormField
control={form.control}
name="sendImmediately"
@@ -340,12 +240,7 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
/>
<DialogFooter className="mt-4">
<Button
variant="secondary"
onClick={() => onOpenChange(false)}
disabled={form.formState.isSubmitting}
type="button"
>
<Button variant="secondary" onClick={() => form.reset()} type="button">
<Trans>Cancel</Trans>
</Button>
@@ -215,7 +215,7 @@ export const TemplateDirectLinkDialog = ({
{DIRECT_TEMPLATE_DOCUMENTATION.map((step, index) => (
<li className="relative" key={index}>
<div className="absolute -left-12">
<div className="flex h-8 w-8 items-center justify-center rounded-full border-[3px] border-neutral-200 font-bold text-sm">
<div className="flex h-8 w-8 items-center justify-center rounded-full border-[3px] border-border font-bold text-sm">
{index + 1}
</div>
</div>
@@ -259,8 +259,8 @@ export const TemplateDirectLinkDialog = ({
.with({ token: P.nullish, currentStep: 'SELECT_RECIPIENT' }, () => (
<DialogContent className="relative">
{isCreatingTemplateDirectLink && validDirectTemplateRecipients.length !== 0 && (
<div className="absolute inset-0 z-50 flex items-center justify-center rounded bg-white/50 dark:bg-black/50">
<LoaderIcon className="h-6 w-6 animate-spin text-gray-500" />
<div className="absolute inset-0 z-50 flex items-center justify-center rounded bg-background/50">
<LoaderIcon className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)}
@@ -317,9 +317,9 @@ export const TemplateDirectLinkDialog = ({
<TableCell>
{selectedRecipientId === row.id ? (
<CircleDotIcon className="h-5 w-5 text-neutral-300" />
<CircleDotIcon className="h-5 w-5 text-muted-foreground/40" />
) : (
<CircleIcon className="h-5 w-5 text-neutral-300" />
<CircleIcon className="h-5 w-5 text-muted-foreground/40" />
)}
</TableCell>
</TableRow>
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
@@ -78,7 +77,6 @@ export const EmbedDirectTemplateClientPage = ({
}: EmbedDirectTemplateClientPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const [searchParams] = useSearchParams();
@@ -266,13 +264,6 @@ export const EmbedDirectTemplateClientPage = ({
const error = AppError.parseError(err);
const errorMessage = getDirectTemplateErrorMessage(error.code);
analytics.captureException(err, {
source: 'embed',
location: 'direct_template',
recipientId: recipient.id,
envelopeId,
});
toast({
title: _(errorMessage.title),
description: _(errorMessage.description),
@@ -317,14 +308,6 @@ export const EmbedDirectTemplateClientPage = ({
}
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'embed_init',
recipientId: recipient.id,
envelopeId,
});
setHasFinishedInit(true);
}
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
@@ -76,7 +75,6 @@ export const EmbedSignDocumentV1ClientPage = ({
}: EmbedSignDocumentV1ClientPageProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const { fullName, email, signature, setFullName, setEmail, setSignature } = useRequiredDocumentSigningContext();
@@ -156,14 +154,6 @@ export const EmbedSignDocumentV1ClientPage = ({
setHasCompletedDocument(true);
} catch (err) {
analytics.captureException(err, {
source: 'embed',
location: 'complete_document',
recipientId: recipient.id,
documentId,
envelopeId,
});
if (window.parent) {
window.parent.postMessage(
{
@@ -246,15 +236,6 @@ export const EmbedSignDocumentV1ClientPage = ({
}
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'embed_init',
recipientId: recipient.id,
documentId,
envelopeId,
});
setHasFinishedInit(true);
}
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
@@ -26,7 +25,6 @@ export const EmbedSignDocumentV2ClientPage = ({
allowWhitelabelling = false,
}: EmbedSignDocumentV2ClientPageProps) => {
const { _ } = useLingui();
const analytics = useAnalytics();
const { envelope, recipient, envelopeData, setFullName, setEmail, fullName, email } =
useRequiredEnvelopeSigningContext();
@@ -172,14 +170,6 @@ export const EmbedSignDocumentV2ClientPage = ({
}
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'embed_init',
recipientId: recipient.id,
envelopeId: envelope.id,
});
setHasFinishedInit(true);
}
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentDataUrlForPdfViewer } from '@documenso/lib/utils/envelope-download';
@@ -58,7 +57,6 @@ export const MultiSignDocumentSigningView = ({
}: MultiSignDocumentSigningViewProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const { fullName, email, signature, setFullName, setSignature } = useRequiredDocumentSigningContext();
@@ -103,13 +101,6 @@ export const MultiSignDocumentSigningView = ({
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'sign_field',
recipientId,
documentId: document?.id,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
@@ -129,13 +120,6 @@ export const MultiSignDocumentSigningView = ({
}
console.error(err);
analytics.captureException(err, {
source: 'embed',
location: 'remove_field',
recipientId,
documentId: document?.id,
});
}
};
@@ -156,13 +140,6 @@ export const MultiSignDocumentSigningView = ({
recipientId,
});
} catch (err) {
analytics.captureException(err, {
source: 'embed',
location: 'complete_document',
recipientId,
documentId: document?.id,
});
onDocumentError?.();
const error = AppError.parseError(err);
@@ -146,7 +146,7 @@ export const AvatarImageForm = ({ className, team, organisation }: AvatarImageFo
<div className="relative">
<Avatar className="h-16 w-16 border-2 border-solid">
{avatarImageId && <AvatarImage src={formatAvatarUrl(avatarImageId)} />}
<AvatarFallback className="text-gray-400 text-sm">{initials}</AvatarFallback>
<AvatarFallback className="text-muted-foreground text-sm">{initials}</AvatarFallback>
</Avatar>
{hasAvatarImage && (
@@ -17,7 +17,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { keepPreviousData } from '@tanstack/react-query';
import { defaultFilter as commandScore } from 'cmdk';
import {
@@ -596,21 +596,9 @@ export const AppCommandMenu = ({ open, onOpenChange }: AppCommandMenuProps) => {
<span className="ml-auto text-muted-foreground text-xs">
{hasValidSearch ? (
isVisibleCountCapped ? (
<Trans>{formatChipCount(totalVisibleCount, isVisibleCountCapped)} results</Trans>
) : (
<Plural
value={totalVisibleCount}
one="# result"
other="# results"
/>
)
<Trans>{formatChipCount(totalVisibleCount, isVisibleCountCapped)} results</Trans>
) : (
<Plural
value={totalVisibleCount}
one="# item"
other="# items"
/>
<Trans>{totalVisibleCount} items</Trans>
)}
</span>
</div>
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { AppError } from '@documenso/lib/errors/app-error';
import type { TTemplate } from '@documenso/lib/types/template';
@@ -42,7 +41,6 @@ export const DirectTemplatePageView = ({
const { _ } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const { email, fullName, setEmail } = useRequiredDocumentSigningContext();
const { recipient, setRecipient } = useRequiredDocumentSigningAuthContext();
@@ -126,13 +124,6 @@ export const DirectTemplatePageView = ({
const error = AppError.parseError(err);
const errorMessage = getDirectTemplateErrorMessage(error.code);
analytics.captureException(err, {
source: 'signing',
location: 'direct_template',
recipientId: directTemplateRecipient.id,
envelopeId: template.envelopeId,
});
toast({
title: _(errorMessage.title),
description: _(errorMessage.description),
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -40,7 +39,6 @@ export const DocumentSigningCheckboxField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -128,13 +126,6 @@ export const DocumentSigningCheckboxField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -166,13 +157,6 @@ export const DocumentSigningCheckboxField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -232,13 +216,6 @@ export const DocumentSigningCheckboxField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while updating the signature.`),
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { type TRecipientAccessAuth, ZDocumentAccessAuthSchema } from '@documenso/lib/types/document-auth';
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
@@ -89,7 +88,6 @@ export const DocumentSigningCompleteDialog = ({
position,
disableNameInput = false,
}: DocumentSigningCompleteDialogProps) => {
const analytics = useAnalytics();
const { t, i18n } = useLingui();
const { toast } = useToast();
@@ -181,11 +179,6 @@ export const DocumentSigningCompleteDialog = ({
return;
}
analytics.captureException(error, {
source: 'signing',
location: 'complete_document',
});
// This dialog owns the completion error toast for every signing surface
// so the user gets a specific, actionable message. Callers should run
// their own side effects (e.g. embeds posting document-error) and
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { convertToLocalSystemFormat, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -40,7 +39,6 @@ export const DocumentSigningDateField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -87,13 +85,6 @@ export const DocumentSigningDateField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -122,13 +113,6 @@ export const DocumentSigningDateField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -36,7 +35,6 @@ export const DocumentSigningDropdownField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -88,13 +86,6 @@ export const DocumentSigningDropdownField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -129,13 +120,6 @@ export const DocumentSigningDropdownField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -34,7 +33,6 @@ export const DocumentSigningEmailField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { email: providedEmail } = useRequiredDocumentSigningContext();
@@ -80,13 +78,6 @@ export const DocumentSigningEmailField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -115,13 +106,6 @@ export const DocumentSigningEmailField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -155,7 +155,7 @@ export const DocumentSigningFieldContainer = ({
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) && field.fieldMeta?.label && (
<div
className={cn(
'absolute -top-16 right-0 left-0 rounded-md p-2 text-center text-gray-700 text-xs',
'absolute -top-16 right-0 left-0 rounded-md p-2 text-center text-foreground text-xs',
{
'border border-border bg-foreground/5': !field.inserted,
},
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -39,7 +38,6 @@ export const DocumentSigningInitialsField = ({
const { toast } = useToast();
const { _ } = useLingui();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { fullName } = useRequiredDocumentSigningContext();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -86,13 +84,6 @@ export const DocumentSigningInitialsField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -121,13 +112,6 @@ export const DocumentSigningInitialsField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -40,7 +39,6 @@ export const DocumentSigningNameField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { fullName: providedFullName, setFullName: setProvidedFullName } = useRequiredDocumentSigningContext();
@@ -118,13 +116,6 @@ export const DocumentSigningNameField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -153,13 +144,6 @@ export const DocumentSigningNameField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,5 +1,4 @@
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -48,7 +47,6 @@ export const DocumentSigningNumberField = ({ field, onSignField, onUnsignField }
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -144,13 +142,6 @@ export const DocumentSigningNumberField = ({ field, onSignField, onUnsignField }
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -202,13 +193,6 @@ export const DocumentSigningNumberField = ({ field, onSignField, onUnsignField }
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -1,3 +1,4 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
@@ -82,6 +83,8 @@ export const DocumentSigningPageViewV1 = ({
? authUser.twoFactorEnabled && authUser.email === recipient.email
: false;
const analytics = useAnalytics();
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
const [isExpanded, setIsExpanded] = useState(false);
@@ -115,6 +118,12 @@ export const DocumentSigningPageViewV1 = ({
await completeDocumentWithToken(payload);
analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id,
documentId: document.id,
timestamp: new Date().toISOString(),
});
if (documentMeta?.redirectUrl) {
window.location.href = documentMeta.redirectUrl;
} else {
@@ -80,7 +80,7 @@ export const DocumentSigningPageViewV2 = () => {
}, [recipientFieldsRemaining, selectedAssistantRecipientFields, currentEnvelopeItem]);
return (
<div className="min-h-screen w-screen bg-gray-50 dark:bg-background">
<div className="min-h-screen w-screen bg-muted dark:bg-background">
<SignFieldEmailDialog.Root />
<SignFieldTextDialog.Root />
<SignFieldNumberDialog.Root />
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -32,7 +31,6 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -93,13 +91,6 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -129,13 +120,6 @@ export const DocumentSigningRadioField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the selection.`),
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
@@ -42,7 +41,6 @@ export function DocumentSigningRejectDialog({
}: DocumentSigningRejectDialogProps) {
const { t } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const [searchParams] = useSearchParams();
const [isOpen, setIsOpen] = useState(false);
@@ -78,12 +76,6 @@ export function DocumentSigningRejectDialog({
window.location.href = `/sign/${token}/rejected`;
}
} catch (err) {
analytics.captureException(err, {
source: 'signing',
location: 'reject_document',
documentId,
});
toast({
title: t`Error`,
description: t`An error occurred while rejecting the document. Please try again.`,
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -48,7 +47,6 @@ export const DocumentSigningSignatureField = ({
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient } = useDocumentSigningRecipientContext();
@@ -159,13 +157,6 @@ export const DocumentSigningSignatureField = ({
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
@@ -192,13 +183,6 @@ export const DocumentSigningSignatureField = ({
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
@@ -1,5 +1,4 @@
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
@@ -51,7 +50,6 @@ export const DocumentSigningTextField = ({ field, onSignField, onUnsignField }:
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const analytics = useAnalytics();
const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
@@ -172,13 +170,6 @@ export const DocumentSigningTextField = ({ field, onSignField, onUnsignField }:
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: isAssistantMode
@@ -209,13 +200,6 @@ export const DocumentSigningTextField = ({ field, onSignField, onUnsignField }:
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'remove_field',
fieldType: field.type,
recipientId: field.recipientId,
});
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while removing the field.`),
@@ -75,7 +75,7 @@ export const DocumentPageViewRecentActivity = ({ documentId, userId }: DocumentP
</div>
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget">
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-border" />
</div>
<button
@@ -109,27 +109,27 @@ export const DocumentPageViewRecentActivity = ({ documentId, userId }: DocumentP
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget text-foreground/40">
{match(auditLog.type)
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, () => (
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
<div className="rounded-full border border-border bg-widget p-1">
<CheckCheckIcon className="h-3 w-3" aria-hidden="true" />
</div>
))
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, () => (
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
<div className="rounded-full border border-border bg-widget p-1">
<CheckIcon className="h-3 w-3" aria-hidden="true" />
</div>
))
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, () => (
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
<div className="rounded-full border border-border bg-widget p-1">
<AlertTriangle className="h-3 w-3" aria-hidden="true" />
</div>
))
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, () => (
<div className="rounded-full border border-gray-300 bg-widget p-1 dark:border-neutral-600">
<div className="rounded-full border border-border bg-widget p-1">
<MailOpen className="h-3 w-3" aria-hidden="true" />
</div>
))
.otherwise(() => (
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-border" />
))}
</div>
@@ -1,4 +1,5 @@
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
@@ -37,6 +38,7 @@ export const DocumentUploadButtonLegacy = ({ className, type }: DocumentUploadBu
const team = useCurrentTeam();
const navigate = useNavigate();
const analytics = useAnalytics();
const organisation = useCurrentOrganisation();
const userTimezone =
@@ -101,6 +103,12 @@ export const DocumentUploadButtonLegacy = ({ className, type }: DocumentUploadBu
description: _(msg`Your document has been uploaded successfully.`),
duration: 5000,
});
analytics.capture('App: Document Uploaded', {
userId: user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
}
// Handle legacy template creation.
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
import type { TLocalField } from '@documenso/lib/client-only/hooks/use-editor-fields';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
@@ -38,12 +37,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { fieldButtonList } from './envelope-editor-fields-drag-drop';
import { EnvelopeRecipientSelectorCommand } from './envelope-recipient-selector';
/** How far past a resize handle you can still grab it, in screen pixels. */
const TRANSFORMER_ANCHOR_HIT_STROKE_PX = 24;
export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageRenderData }) => {
const { t, i18n } = useLingui();
const analytics = useAnalytics();
const { envelope, editorFields, getRecipientColorKey } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
@@ -281,13 +276,6 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
unsafeRenderFieldOnLayer(field);
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'editor',
location: 'envelope_page_render',
envelopeId: envelope.id,
});
setRenderError(true);
}
};
@@ -362,9 +350,6 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR
shouldOverdrawWholeArea: true,
ignoreStroke: true,
flipEnabled: false,
anchorStyleFunc: (anchor) => {
anchor.hitStrokeWidth(TRANSFORMER_ANCHOR_HIT_STROKE_PX / scale);
},
boundBoxFunc: (oldBox, newBox) => {
// Enforce minimum size
if (newBox.width < 30 || newBox.height < 20) {
@@ -264,7 +264,7 @@ export const EnvelopeEditorPreviewPage = () => {
{/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && false && (
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-gray-200 border-l bg-white py-4">
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-border border-l bg-background py-4">
{/* Add fields section. */}
<section className="px-4">
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900">
@@ -329,7 +329,7 @@ export const EnvelopeEditorPreviewPage = () => {
{/* Recipient selector section. */}
<section className="px-4">
<h3 className="mb-2 font-semibold text-gray-900 text-sm">
<h3 className="mb-2 font-semibold text-foreground text-sm">
<Trans>Selected Recipient</Trans>
</h3>
@@ -62,7 +62,7 @@ export const EnvelopeItemTitleInput = ({
{/* Hidden span to measure text width */}
<span
ref={measureRef}
className="pointer-events-none absolute top-0 left-0 whitespace-nowrap font-medium text-gray-600 text-sm opacity-0"
className="pointer-events-none absolute top-0 left-0 whitespace-nowrap font-medium text-muted-foreground text-sm opacity-0"
style={{ font: 'inherit' }}
>
{envelopeItemTitle || placeholder}
@@ -1,5 +1,4 @@
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useEnvelopeAutosave } from '@documenso/lib/client-only/hooks/use-envelope-autosave';
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
@@ -46,7 +45,6 @@ export const EnvelopeEditorUploadPage = () => {
const { t, i18n } = useLingui();
const { maximumEnvelopeItemCount, remaining } = useLimits();
const { toast } = useToast();
const analytics = useAnalytics();
const {
envelope,
@@ -215,12 +213,6 @@ export const EnvelopeEditorUploadPage = () => {
const { data } = await createPromise.catch((error) => {
console.error(error);
analytics.captureException(error, {
source: isEmbedded ? 'embed' : 'editor',
location: 'create_envelope_items',
envelopeId: envelope.id,
});
// Set error state on files in batch upload.
setLocalFiles((prev) =>
prev.map((uploadingFile) =>
@@ -298,12 +290,6 @@ export const EnvelopeEditorUploadPage = () => {
} catch (error) {
console.error(error);
analytics.captureException(error, {
source: isEmbedded ? 'embed' : 'editor',
location: 'replace_pdf',
envelopeId: envelope.id,
});
toast({
title: t`Replace failed`,
description: t`Something went wrong while replacing the PDF`,
@@ -271,7 +271,7 @@ export const EnvelopeEditor = () => {
`cursor-pointer rounded-lg text-left transition-colors ${
isActive
? 'border border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
: 'border border-gray-200 hover:bg-gray-50 dark:border-gray-400/20 dark:hover:bg-gray-400/10'
: 'border border-border hover:bg-muted/50'
}`,
{
'p-3': !minimizeLeftSidebar,
@@ -284,10 +284,10 @@ export const EnvelopeEditor = () => {
className={`rounded border p-2 ${
isActive
? 'border-green-200 bg-green-50 dark:border-green-500/20 dark:bg-green-500/10'
: 'border-gray-100 bg-gray-100 dark:border-gray-400/20 dark:bg-gray-400/10'
: 'border-muted bg-muted'
}`}
>
<Icon className={`h-4 w-4 ${isActive ? 'text-green-600' : 'text-gray-600'}`} />
<Icon className={`h-4 w-4 ${isActive ? 'text-green-600' : 'text-muted-foreground'}`} />
</div>
{!minimizeLeftSidebar && (
@@ -31,14 +31,14 @@ export const EnvelopeItemSelector = ({
>
<div
className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full font-medium text-xs ${
isSelected ? 'bg-green-100 text-green-600' : 'bg-gray-200 text-gray-600'
isSelected ? 'bg-green-100 text-green-600' : 'bg-muted text-muted-foreground'
}`}
>
{number}
</div>
<div className="min-w-0 text-left">
<div className="truncate font-medium text-sm">{primaryText}</div>
<div className="text-gray-500 text-xs">{secondaryText}</div>
<div className="text-muted-foreground text-xs">{secondaryText}</div>
</div>
{actionSlot ?? (
<div
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
import {
type PageRenderData,
@@ -19,7 +18,6 @@ type GenericLocalField = TEnvelope['fields'][number] & {
export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRenderData }) => {
const { i18n } = useLingui();
const analytics = useAnalytics();
const {
envelopeStatus,
@@ -116,13 +114,6 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender
unsafeRenderFieldOnLayer(field);
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'editor',
location: 'envelope_page_render',
envelopeId: currentEnvelopeItem?.envelopeId,
});
setRenderError(true);
}
};
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
import {
type PageRenderData,
@@ -54,7 +53,6 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { toast } = useToast();
const analytics = useAnalytics();
const {
envelopeData,
@@ -428,14 +426,6 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
unsafeRenderFieldOnLayer(unparsedField, fieldCanvasStyleCache);
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'page_render',
recipientId: recipient.id,
envelopeId: envelope.id,
});
setRenderError(true);
}
};
@@ -517,14 +507,6 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
} catch (err) {
console.error(err);
analytics.captureException(err, {
source: 'signing',
location: 'sign_field',
fieldType: payload.type,
recipientId: recipient.id,
envelopeId: envelope.id,
});
toast({
title: t`Error`,
description: t`An error occurred while signing the field.`,
@@ -118,6 +118,12 @@ export const EnvelopeSignerCompleteDialog = () => {
title: t`Document already signed`,
description: t`This document was already signed and no further action was taken.`,
});
} else {
analytics.capture('App: Recipient has completed signing', {
signerId: recipient.id,
documentId: envelope.id,
timestamp: new Date().toISOString(),
});
}
if (onDocumentCompleted) {
@@ -142,13 +148,6 @@ export const EnvelopeSignerCompleteDialog = () => {
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.TWO_FACTOR_AUTH_FAILED) {
analytics.captureException(err, {
source: 'signing',
location: 'complete_document',
recipientId: recipient.id,
envelopeId: envelope.id,
});
onDocumentError?.();
}
@@ -222,13 +221,6 @@ export const EnvelopeSignerCompleteDialog = () => {
} catch (err) {
console.log('err', err);
analytics.captureException(err, {
source: 'signing',
location: 'complete_document_next_signer',
recipientId: recipient.id,
envelopeId: envelope.id,
});
onDocumentError?.();
// Rethrow so DocumentSigningCompleteDialog can toast a specific
@@ -93,6 +93,14 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
duration: 5000,
});
if (type === EnvelopeType.DOCUMENT) {
analytics.capture('App: Document Uploaded', {
userId: user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
}
const pathPrefix = type === EnvelopeType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
const aiQueryParam = team.preferences.aiFeaturesEnabled ? '?ai=true' : '';
@@ -101,11 +109,6 @@ export const EnvelopeDropZoneWrapper = ({ children, type, className }: EnvelopeD
} catch (err) {
const error = AppError.parseError(err);
analytics.captureException(err, {
source: 'editor',
location: 'upload_document',
});
const errorMessage = getUploadErrorMessage(error.code);
toast({
@@ -1,5 +1,4 @@
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
@@ -35,7 +34,6 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
const { t, i18n } = useLingui();
const { toast } = useToast();
const { user } = useSession();
const analytics = useAnalytics();
const team = useCurrentTeam();
@@ -114,11 +112,6 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
console.error(err);
analytics.captureException(err, {
source: 'editor',
location: 'upload_document',
});
const errorMessage = getUploadErrorMessage(error.code);
toast({
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import type { ImageLoadingState, PageRenderData } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { PDF_VIEWER_PAGE_CLASSNAME } from '@documenso/lib/constants/pdf-viewer';
import { cn } from '@documenso/ui/lib/utils';
@@ -69,7 +68,6 @@ export default function PDFViewer({
}: PDFViewerProps) {
const { t } = useLingui();
const { toast } = useToast();
const analytics = useAnalytics();
const $el = useRef<HTMLDivElement>(null);
@@ -152,11 +150,6 @@ export default function PDFViewer({
console.error(err);
setLoadingState('error');
analytics.captureException(err, {
source: 'pdf_viewer',
location: 'pdf_load',
});
toast({
title: t`Error`,
description: t`An error occurred while loading the document.`,
@@ -373,8 +366,6 @@ const PdfViewerPage = ({
* Manages rendering a page from a pdf.
*/
const usePdfPageImage = ({ pageNumber, pdf, scale, scaledWidth, scaledHeight }: PdfViewerPageProps) => {
const analytics = useAnalytics();
const [imageLoadingState, setImageLoadingState] = useState<ImageLoadingState>('loading');
const [imageUrl, setImageUrl] = useState('');
@@ -466,12 +457,6 @@ const usePdfPageImage = ({ pageNumber, pdf, scale, scaledWidth, scaledHeight }:
if (!isCancelled) {
console.error(err);
analytics.captureException(err, {
source: 'pdf_viewer',
location: 'pdf_page_render',
});
setImageLoadingState('error');
}
} finally {
@@ -31,7 +31,7 @@ export const TemplateDirectLinkBadge = ({ token, enabled, className }: TemplateD
<button
title="Copy direct link"
className={cn(
'flex flex-row items-center rounded border border-neutral-300 bg-neutral-200 px-1.5 py-0.5 text-xs dark:border-neutral-500 dark:bg-neutral-600',
'flex flex-row items-center rounded border border-border bg-muted px-1.5 py-0.5 text-xs',
className,
)}
onClick={async () => onCopyClick(token)}
@@ -71,7 +71,7 @@ export const TemplatePageViewRecentActivity = ({
</div>
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget">
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-border" />
</div>
<button
@@ -108,7 +108,7 @@ export const TemplatePageViewRecentActivity = ({
</div>
<div className="relative flex h-6 w-6 flex-none items-center justify-center bg-widget text-foreground/40">
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-gray-300 dark:ring-neutral-600" />
<div className="h-1.5 w-1.5 rounded-full bg-widget ring-1 ring-border" />
</div>
<Link
@@ -39,14 +39,13 @@ const ADMIN_GROUP_ICONS: Record<TAdminSearchResultType, LucideIcon> = {
/**
* Admin list pages which support prefilling their search from the URL, used
* for the "View all results" links on capped groups. Teams and
* for the "View all results" links on capped groups. Teams, recipients and
* subscriptions have no admin list pages.
*/
const ADMIN_GROUP_LIST_PATHS: Partial<Record<TAdminSearchResultType, (_query: string) => string>> = {
document: (query) => `/admin/documents?term=${encodeURIComponent(query)}`,
user: (query) => `/admin/users?search=${encodeURIComponent(query)}`,
organisation: (query) => `/admin/organisations?query=${encodeURIComponent(query)}`,
recipient: (query) => `/admin/documents?term=${encodeURIComponent(`recipient:${query}`)}`,
};
export type UseAdminSearchCategoriesOptions = {
@@ -127,8 +127,8 @@ export const AdminDashboardUsersTable = ({ users, totalPages, perPage, page }: A
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -204,8 +204,8 @@ export const AdminOrganisationOverviewTable = ({
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -110,7 +110,7 @@ export const AdminOrganisationsTable = ({
return (
<div
className={`inline-flex items-center rounded-full px-2 py-1 font-medium text-xs ${
isPaid ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
isPaid ? 'bg-green-100 text-green-800' : 'bg-muted text-muted-foreground'
}`}
>
{isPaid ? (
@@ -253,8 +253,8 @@ export const OrganisationInsightsTable = ({
</div>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -72,7 +72,7 @@ export const SettingsPublicProfileTemplatesTable = () => {
return (
<div>
<div className="mt-6 divide-y divide-neutral-200 overflow-hidden rounded-lg border border-neutral-200 dark:divide-foreground/30 dark:border-foreground/30">
<div className="mt-6 divide-y divide-border overflow-hidden rounded-lg border border-border">
{/* Loading and error handling states. */}
{publicDirectTemplates.length === 0 && (
<>
@@ -132,7 +132,7 @@ export const SettingsPublicProfileTemplatesTable = () => {
<div>
<p className="break-all text-sm">{template.publicTitle}</p>
<p className="break-all text-neutral-400 text-xs">{template.publicDescription}</p>
<p className="break-all text-muted-foreground text-xs">{template.publicDescription}</p>
</div>
</div>
@@ -134,7 +134,7 @@ export const TemplatesTable = ({
</p>
</li>
<li>
<div className="mb-2 flex w-fit flex-row items-center rounded border border-neutral-300 bg-neutral-200 px-1.5 py-0.5 text-xs dark:border-neutral-500 dark:bg-neutral-600">
<div className="mb-2 flex w-fit flex-row items-center rounded border border-border bg-muted px-1.5 py-0.5 text-xs">
<Link2Icon className="mr-1 h-3 w-3" />
<Trans>direct link</Trans>
</div>
@@ -310,8 +310,8 @@ export const TemplatesTable = ({
</DataTable>
{isPending && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
-44
View File
@@ -18,23 +18,6 @@ import './utils/polyfills/promise-with-resolvers';
* the page early, leaving dead event handlers (broken dropdowns, native form
* submits).
*/
/**
* Signing and direct template URLs contain recipient tokens which must never
* be sent to PostHog. Recipient context is attached explicitly via
* `recipientId` where needed instead.
*/
const redactTokensFromUrl = (value: string) => {
return value.replace(/(\/(?:sign|d|direct)\/)([^/?#]+)/g, '$1:token');
};
const URL_EVENT_PROPERTIES = [
'$current_url',
'$pathname',
'$referrer',
'$initial_referrer',
'$prev_pageview_pathname',
] as const;
function initPosthog() {
const postHogConfig = extractPostHogConfig();
@@ -42,34 +25,7 @@ function initPosthog() {
void import('posthog-js').then(({ default: posthog }) => {
posthog.init(postHogConfig.key, {
api_host: postHogConfig.host,
// Only create person profiles for identified (authenticated) users,
// anonymous recipients on signing pages stay anonymous.
person_profiles: 'identified_only',
// Explicit events only, autocapture on signing pages blows up usage
// without providing actionable data.
autocapture: false,
capture_pageview: true,
capture_pageleave: false,
capture_exceptions: true,
before_send: (event) => {
if (!event) {
return null;
}
for (const property of URL_EVENT_PROPERTIES) {
const value = event.properties?.[property];
if (typeof value === 'string') {
event.properties[property] = redactTokensFromUrl(value);
}
}
if (event.$set_once && typeof event.$set_once['$initial_current_url'] === 'string') {
event.$set_once['$initial_current_url'] = redactTokensFromUrl(event.$set_once['$initial_current_url']);
}
return event;
},
});
});
}
+3 -4
View File
@@ -7,11 +7,10 @@ import { createReadableStreamFromReadable } from '@react-router/node';
import { isbot } from 'isbot';
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
import type { EntryContext, RouterContextProvider } from 'react-router';
import type { AppLoadContext, EntryContext } from 'react-router';
import { ServerRouter } from 'react-router';
import { langCookie } from './storage/lang-cookie.server';
import { nonceContext } from './utils/nonce';
export const streamTimeout = 5_000;
@@ -20,7 +19,7 @@ export default async function handleRequest(
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: RouterContextProvider,
loadContext: AppLoadContext,
) {
let language = await langCookie.parse(request.headers.get('cookie') ?? '');
@@ -34,7 +33,7 @@ export default async function handleRequest(
// scripts it injects (route manifest, hydration data, module preloads).
// The same nonce is also exposed to the React tree via the root loader so
// our own inline scripts/styles can carry it.
const nonce = loadContext.get(nonceContext) || undefined;
const nonce = loadContext.nonce || undefined;
return new Promise((resolve, reject) => {
let shellRendered = false;
-13
View File
@@ -1,13 +0,0 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { isAdmin } from '@documenso/lib/utils/is-admin';
import { type MiddlewareFunction, redirect } from 'react-router';
export const adminMiddleware: MiddlewareFunction = async ({ request }, next) => {
const { user } = await getOptionalSession(request);
if (!user || !isAdmin(user)) {
throw redirect('/');
}
return next();
};
-8
View File
@@ -1,8 +0,0 @@
import type { MiddlewareFunction } from 'react-router';
import { getRequestNonce } from '../../server/load-context';
import { nonceContext } from '../utils/nonce';
export const nonceMiddleware: MiddlewareFunction = ({ context }) => {
context.set(nonceContext, getRequestNonce());
};
+2 -15
View File
@@ -1,5 +1,4 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { SessionProvider } from '@documenso/lib/client-only/providers/session';
import { getBasePath } from '@documenso/lib/constants/app';
import { APP_I18N_OPTIONS, type SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
@@ -10,7 +9,6 @@ import { getOrganisationSession } from '@documenso/trpc/server/organisation-rout
import { Toaster } from '@documenso/ui/primitives/toaster';
import { TooltipProvider } from '@documenso/ui/primitives/tooltip';
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7';
import { useEffect } from 'react';
import {
data,
isRouteErrorResponse,
@@ -23,16 +21,13 @@ import {
useMatches,
} from 'react-router';
import { PreventFlashOnWrongTheme, ThemeProvider, useTheme } from 'remix-themes';
import { nonceMiddleware } from '~/middleware/nonce';
import type { Route } from './+types/root';
import stylesheet from './app.css?url';
import { GenericErrorLayout } from './components/general/generic-error-layout';
import { langCookie } from './storage/lang-cookie.server';
import { themeSessionResolver } from './storage/theme-session.server';
import { appMetaTags } from './utils/meta';
import { nonce, nonceContext } from './utils/nonce';
export const middleware = [nonceMiddleware];
import { nonce } from './utils/nonce';
export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }];
@@ -77,7 +72,7 @@ export async function loader({ context, request }: Route.LoaderArgs) {
// Surface the per-request CSP nonce produced by `securityHeadersMiddleware` so all
// SSR-rendered <script>/<style> elements in this layout (and child
// routes that need it) can carry the matching nonce attribute.
nonce: context.get(nonceContext),
nonce: context.nonce,
session: session.isAuthenticated
? {
user: session.user,
@@ -205,19 +200,11 @@ export default function App() {
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
if (errorCode !== 404) {
console.error('[RootErrorBoundary]', error);
}
useEffect(() => {
if (errorCode !== 404) {
analytics.captureException(error, { source: 'app', location: 'root_boundary' });
}
}, [error]);
return <GenericErrorLayout errorCode={errorCode} />;
}
@@ -1,4 +1,4 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
import { isAdmin } from '@documenso/lib/utils/is-admin';
import { cn } from '@documenso/ui/lib/utils';
@@ -20,18 +20,16 @@ import {
import { Link, Outlet, redirect, useLocation } from 'react-router';
import { AdminLicenseStatusBanner } from '~/components/general/admin-license-status-banner';
import { adminMiddleware } from '~/middleware/admin';
import { appMetaTags } from '~/utils/meta';
import type { Route } from './+types/_layout';
export function meta() {
return appMetaTags(msg`Admin`);
}
export const middleware = [adminMiddleware];
export async function loader({ request }: Route.LoaderArgs) {
const { user } = await getOptionalSession(request);
const { user } = await getSession(request);
const license = await LicenseClient.getInstance()?.getCachedLicense();
@@ -12,7 +12,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { Link, useSearchParams } from 'react-router';
import { DocumentStatus } from '~/components/general/document/document-status';
@@ -30,12 +30,6 @@ export default function AdminDocumentsPage() {
const page = searchParams?.get?.('page') ? Number(searchParams.get('page')) : undefined;
const perPage = searchParams?.get?.('perPage') ? Number(searchParams.get('perPage')) : undefined;
const urlTerm = searchParams?.get?.('term') ?? '';
useEffect(() => {
setTerm(urlTerm);
}, [urlTerm]);
const { data: findDocumentsData, isPending: isFindDocumentsLoading } = trpc.admin.document.find.useQuery(
{
query: debouncedTerm,
@@ -137,7 +131,7 @@ export default function AdminDocumentsPage() {
<div>
<Input
type="search"
placeholder={_(msg`Search by document title, recipient:123, team:123 or user:123`)}
placeholder={_(msg`Search by document title, team:123 or user:123`)}
value={term}
onChange={(e) => setTerm(e.target.value)}
/>
@@ -155,8 +149,8 @@ export default function AdminDocumentsPage() {
</DataTable>
{isFindDocumentsLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -182,8 +182,8 @@ export default function AdminEmailDomainsPage() {
</DataTable>
{isFindEmailDomainsLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -173,8 +173,8 @@ export default function AdminUnsealedDocumentsPage() {
</DataTable>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
</div>
@@ -108,7 +108,7 @@ export default function DashboardPage() {
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10 border border-solid">
{org.avatarImageId && <AvatarImage src={formatAvatarUrl(org.avatarImageId)} />}
<AvatarFallback className="text-gray-400 text-sm">
<AvatarFallback className="text-muted-foreground text-sm">
{org.name.slice(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
@@ -182,7 +182,7 @@ export default function DashboardPage() {
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10 border border-solid">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-gray-400 text-sm">
<AvatarFallback className="text-muted-foreground text-sm">
{team.name.slice(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
@@ -129,7 +129,7 @@ export default function OrganisationSettingsTeamsPage() {
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10 border-2 border-solid">
{team.avatarImageId && <AvatarImage src={formatAvatarUrl(team.avatarImageId)} />}
<AvatarFallback className="text-gray-400 text-sm">
<AvatarFallback className="text-muted-foreground text-sm">
{team.name.slice(0, 1).toUpperCase()}
</AvatarFallback>
</Avatar>
@@ -1,5 +1,4 @@
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -8,7 +7,6 @@ import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { ChevronLeftIcon } from 'lucide-react';
import { useEffect } from 'react';
import type { ShouldRevalidateFunctionArgs } from 'react-router';
import { isRouteErrorResponse, Link, Outlet, redirect } from 'react-router';
@@ -68,18 +66,8 @@ export default function DocumentsLayout() {
}
export function ErrorBoundary({ error, params }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, {
source: 'editor',
location: 'editor_layout_boundary',
envelopeId: params.id,
});
}, [error]);
const errorCodeMap = {
404: {
subHeading: msg`404 Document not found`,
@@ -68,20 +68,6 @@ export default function ApiTokensPage() {
return i18n.date(row.original.expires);
},
},
{
header: t`Last Used`,
cell: ({ row }) => {
if (!row.original.lastUsedAt) {
return (
<span className="text-muted-foreground">
<Trans>Never</Trans>
</span>
);
}
return <span className="text-foreground">{i18n.date(row.original.lastUsedAt)}</span>;
},
},
{
header: t`Actions`,
cell: ({ row }) => (
@@ -160,9 +146,6 @@ export default function ApiTokensPage() {
<TableCell>
<Skeleton className="h-4 w-16 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-16 rounded-full" />
</TableCell>
<TableCell>
<Skeleton className="h-4 w-12 rounded-full" />
</TableCell>
@@ -96,8 +96,8 @@ export default function WebhookPage() {
<WebhookCreateDialog />
</SettingsHeader>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/50">
<Loader className="h-8 w-8 animate-spin text-gray-500" />
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
@@ -1,5 +1,4 @@
import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
@@ -9,7 +8,6 @@ import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { ChevronLeftIcon } from 'lucide-react';
import { useEffect } from 'react';
import { isRouteErrorResponse, Link, Outlet, redirect, type ShouldRevalidateFunctionArgs } from 'react-router';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
@@ -86,18 +84,8 @@ export default function TemplatesLayout() {
}
export function ErrorBoundary({ error, params }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, {
source: 'editor',
location: 'editor_layout_boundary',
envelopeId: params.id,
});
}, [error]);
const errorCodeMap = {
404: {
subHeading: msg`404 Template not found`,
+4 -2
View File
@@ -74,7 +74,9 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
<Avatar className="h-24 w-24 border-2 border-solid dark:border-border">
{publicProfile.avatarImageId && <AvatarImage src={formatAvatarUrl(publicProfile.avatarImageId)} />}
<AvatarFallback className="text-gray-400 text-sm">{extractInitials(publicProfile.name)}</AvatarFallback>
<AvatarFallback className="text-muted-foreground text-sm">
{extractInitials(publicProfile.name)}
</AvatarFallback>
</Avatar>
<div className="mt-4 flex flex-row items-center justify-center">
@@ -153,7 +155,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
<Table className="w-full" overflowHidden>
<TableHeader>
<TableRow>
<TableHead className="w-full rounded-tl-md bg-neutral-50 dark:bg-neutral-700">
<TableHead className="w-full rounded-tl-md bg-muted">
<Trans>Documents</Trans>
</TableHead>
</TableRow>
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
@@ -6,7 +5,6 @@ import { i18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react';
import { useEffect } from 'react';
import { isRouteErrorResponse, Link, Outlet } from 'react-router';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
@@ -50,14 +48,8 @@ export default function RecipientLayout({ matches }: Route.ComponentProps) {
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, { source: 'signing', location: 'recipient_layout_boundary' });
}, [error]);
return (
<GenericErrorLayout
errorCode={errorCode}
@@ -1,8 +1,8 @@
import { getCertificateStatus } from '@documenso/lib/server-only/cert/cert-status';
export const loader = async () => {
export const loader = () => {
try {
const certStatus = await getCertificateStatus();
const certStatus = getCertificateStatus();
return Response.json({
isAvailable: certStatus.isAvailable,
+1 -1
View File
@@ -22,7 +22,7 @@ export const loader = async () => {
}
try {
const certStatus = await getCertificateStatus();
const certStatus = getCertificateStatus();
if (certStatus.isAvailable) {
checks.certificate = { status: 'ok' };
@@ -1,4 +1,3 @@
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import {
IS_GOOGLE_SSO_ENABLED,
IS_MICROSOFT_SSO_ENABLED,
@@ -6,7 +5,6 @@ import {
OIDC_PROVIDER_LABEL,
} from '@documenso/lib/constants/auth';
import { Trans } from '@lingui/react/macro';
import { useEffect } from 'react';
import { isRouteErrorResponse, Outlet, useRouteError } from 'react-router';
import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required';
@@ -49,30 +47,10 @@ export default function Layout() {
export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData || {};
const analytics = useAnalytics();
const error = useRouteError();
console.log({ routeError: error });
useEffect(() => {
const isExpectedEmbedResponse =
isRouteErrorResponse(error) &&
[
'embed-authentication-required',
'embed-paywall',
'embed-waiting-for-turn',
'embed-recipient-expired',
'embed-document-rejected',
'embed-document-completed',
].includes(error.data?.type);
if (isExpectedEmbedResponse) {
return;
}
analytics.captureException(error, { source: 'embed', location: 'embed_layout_boundary' });
}, [error]);
if (isRouteErrorResponse(error)) {
if (error.status === 401 && error.data.type === 'embed-authentication-required') {
return (
@@ -2,13 +2,11 @@ import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { getEnvelopeForDirectTemplateSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-direct-template-signing';
import { getEnvelopeRequiredAccessData } from '@documenso/lib/server-only/envelope/get-envelope-required-access-data';
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { data } from 'react-router';
@@ -95,30 +93,6 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
const fields = template.fields.filter((field) => field.recipientId === directTemplateRecipientId);
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: template.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: template.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: template.envelopeId,
},
});
});
return {
token,
user,
@@ -220,30 +194,6 @@ async function handleV2Loader({ params, request }: Route.LoaderArgs) {
);
}
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: envelope.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: envelope.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: envelope.id,
},
});
});
return {
token,
user,
@@ -2,7 +2,6 @@ import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getEnvelopeForRecipientSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
@@ -14,7 +13,6 @@ import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { isRecipientExpired } from '@documenso/lib/utils/recipients';
@@ -141,30 +139,6 @@ async function handleV1Loader({ params, request }: Route.LoaderArgs) {
})
: [];
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: document.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: document.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: document.envelopeId,
},
});
});
return {
token,
user,
@@ -298,30 +272,6 @@ async function handleV2Loader({ params, request }: Route.LoaderArgs) {
recipientAccessAuth: derivedRecipientAccessAuth,
}).catch(() => null);
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: envelope.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: envelope.teamId,
properties: {
type: 'signing',
version: 'v0',
recipientId: recipient.id,
envelopeId: envelope.id,
},
});
});
return {
token,
user,
@@ -1,11 +1,8 @@
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { ZBaseEmbedAuthoringSchema } from '@documenso/lib/types/embed-authoring-base-schema';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { dynamicActivate } from '@documenso/lib/utils/i18n';
import { prisma } from '@documenso/prisma';
import { TrpcProvider } from '@documenso/trpc/react';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { Trans } from '@lingui/react/macro';
@@ -38,40 +35,6 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
});
allowEmbedAuthoringWhiteLabel = organisationClaim.flags.embedAuthoringWhiteLabel ?? false;
// Derive the kind of authoring session from the child route path, e.g.
// /embed/v1/authoring/document/create -> resource 'document', mode 'create'.
// Completed and error pages are not new sessions and are skipped.
const [resource, mode] = url.pathname.split('/').filter(Boolean).slice(3);
const isAuthoringSession =
(resource === 'document' || resource === 'template') && (mode === 'create' || mode === 'edit');
if (isAuthoringSession) {
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: result.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: result.userId,
organisationId: team?.organisationId,
teamId: result.teamId,
properties: {
type: 'authoring',
version: 'v1',
resource,
mode,
},
});
});
}
}
return {
@@ -1,11 +1,8 @@
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { ZSignDocumentEmbedDataSchema } from '@documenso/lib/types/embed-document-sign-schema';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { prisma } from '@documenso/prisma';
import { Trans } from '@lingui/react/macro';
import { SigningStatus } from '@prisma/client';
import { useEffect, useLayoutEffect, useState } from 'react';
@@ -45,15 +42,6 @@ export async function loader({ request }: Route.LoaderArgs) {
const firstDocument = envelopes[0]?.document;
if (!firstDocument) {
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
properties: {
type: 'signing',
version: 'v1',
},
});
return superLoaderJson({
envelopes,
user,
@@ -67,29 +55,6 @@ export async function loader({ request }: Route.LoaderArgs) {
const allowWhitelabelling = organisationClaim.flags.embedSigningWhiteLabel;
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
fireAndForget(async () => {
const team = await prisma.team.findFirst({
where: {
id: firstDocument.teamId,
},
select: {
organisationId: true,
},
});
captureServerEvent({
event: 'App: Embed Session Started',
userId: user?.id,
organisationId: team?.organisationId,
teamId: firstDocument.teamId,
properties: {
type: 'signing',
version: 'v1',
envelopeId: firstDocument.envelopeId,
},
});
});
return superLoaderJson({
envelopes,
user,
@@ -1,6 +1,5 @@
import { PAID_PLAN_LIMITS } from '@documenso/ee/server-only/limits/constants';
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { OrganisationProvider } from '@documenso/lib/client-only/providers/organisation';
import { APP_I18N_OPTIONS } from '@documenso/lib/constants/i18n';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
@@ -13,7 +12,7 @@ import type { OrganisationSession } from '@documenso/trpc/server/organisation-ro
import { Spinner } from '@documenso/ui/primitives/spinner';
import { Trans } from '@lingui/react/macro';
import { OrganisationMemberRole, OrganisationType, TeamMemberRole } from '@prisma/client';
import { useEffect, useLayoutEffect, useState } from 'react';
import { useLayoutEffect, useState } from 'react';
import { isRouteErrorResponse, Outlet, useLoaderData } from 'react-router';
import { match } from 'ts-pattern';
@@ -167,17 +166,8 @@ export default function AuthoringLayout() {
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
const analytics = useAnalytics();
const errorCode = isRouteErrorResponse(error) ? error.status : 500;
useEffect(() => {
analytics.captureException(error, {
source: 'embed',
location: 'embed_authoring_boundary',
});
}, [error]);
return (
<div>
{match(errorCode)
@@ -1,6 +1,5 @@
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
@@ -11,7 +10,6 @@ import {
ZEmbedCreateEnvelopeAuthoringSchema,
} from '@documenso/lib/types/envelope-editor';
import type { TEnvelopeFieldAndMeta } from '@documenso/lib/types/field-meta';
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
import { extractDerivedDocumentMeta } from '@documenso/lib/utils/document';
import { buildEmbeddedEditorOptions, buildEmbeddedFeatures } from '@documenso/lib/utils/embed-config';
import { prisma } from '@documenso/prisma';
@@ -78,30 +76,6 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
},
});
fireAndForget(async () => {
const team = result.teamId
? await prisma.team.findFirst({
where: {
id: result.teamId,
},
select: {
organisationId: true,
},
})
: null;
captureServerEvent({
event: 'App: Embed Session Started',
userId: result.userId,
organisationId: team?.organisationId,
teamId: result.teamId ?? undefined,
properties: {
type: 'authoring',
version: 'v2',
},
});
});
return superLoaderJson({
token,
tokenUserId: result.userId,
@@ -1,6 +1,5 @@
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getEditorEnvelopeById } from '@documenso/lib/server-only/envelope/get-editor-envelope-by-id';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
@@ -95,18 +94,6 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
brandingLogo = settings.brandingLogo;
}
captureServerEvent({
event: 'App: Embed Session Started',
userId: result.userId,
organisationId: envelope.team.organisationId,
teamId: result.teamId ?? undefined,
properties: {
type: 'authoring',
version: 'v2',
envelopeId: envelope.id,
},
});
return superLoaderJson({
token,
envelope,
+1 -7
View File
@@ -1,10 +1,4 @@
import { createContext, useRouteLoaderData } from 'react-router';
/**
* Per-request CSP nonce. Set by the root route middleware, read with
* `context.get(nonceContext)` in loaders/actions and `entry.server`.
*/
export const nonceContext = createContext<string>('');
import { useRouteLoaderData } from 'react-router';
/**
* Returns the supplied CSP nonce only when rendering on the server.
+1 -1
View File
@@ -106,5 +106,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "2.18.0"
"version": "2.16.0"
}
-3
View File
@@ -8,7 +8,4 @@ export default {
// kept without a trailing slash so they match exactly, and so the bare
// sub-path URL (e.g. "/ESign") still matches the basename at runtime.
basename: process.env.NEXT_PUBLIC_BASE_PATH ? process.env.NEXT_PUBLIC_BASE_PATH.replace(/\/$/, '') : '/',
future: {
v8_middleware: true,
},
} satisfies Config;
+3 -2
View File
@@ -24,11 +24,12 @@ export const appContext = async (c: Context, next: Next) => {
// These are non page paths like API.
if (!isPageRequest(request) || noSessionCookie || blacklistedPathsRegex.test(url.pathname)) {
return await next();
return next();
}
// Add context to any pages you want here.
return await next();
return next();
};
const setAppContext = (c: Context, context: AppContext) => {
+23 -6
View File
@@ -1,16 +1,33 @@
import { getContext } from 'hono/context-storage';
import { RouterContextProvider } from 'react-router';
import type { AppLoadContext } from 'react-router';
import type { HonoEnv } from './router';
import { CSP_NONCE_KEY } from './security-headers';
/**
* Per-request CSP nonce set by `securityHeadersMiddleware`, read via
* `hono/context-storage` (enabled in `server/router.ts`).
* Augment React Router's `AppLoadContext` so loaders, actions, and
* `entry.server` can access fields by name without casts.
*/
export const getRequestNonce = (): string => getContext<HonoEnv>().var[CSP_NONCE_KEY] ?? '';
declare module 'react-router' {
interface AppLoadContext {
/**
* Per-request CSP nonce. Populated by `securityHeadersMiddleware` and surfaced here
* so it can be threaded into `<ServerRouter nonce>` and root loader
* data, which then feeds `<Scripts>`, `<Links>`, etc.
*/
nonce: string;
}
}
/**
* `future.v8_middleware` requires a `RouterContextProvider` instance here.
* Builds the React Router `AppLoadContext` for both dev (vite plugin) and
* production (`hono-react-router-adapter/node`).
*
* The Hono context isn't passed directly by the adapter, so we read it via
* `hono/context-storage`, which is enabled in `server/router.ts`.
*/
export const getLoadContext = (): RouterContextProvider => new RouterContextProvider();
export const getLoadContext = (): AppLoadContext => {
const nonce = getContext<HonoEnv>().var[CSP_NONCE_KEY] ?? '';
return { nonce };
};
+1 -2
View File
@@ -5,7 +5,6 @@ import { lingui } from '@lingui/vite-plugin';
import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer';
import serverAdapter from 'hono-react-router-adapter/vite';
import type { AppLoadContext } from 'react-router';
import tailwindcss from 'tailwindcss';
import { defineConfig, normalizePath } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros';
@@ -54,7 +53,7 @@ export default defineConfig({
entry: 'server/router.ts',
getLoadContext: async () => {
const { getLoadContext } = await import('./server/load-context');
return getLoadContext() as unknown as AppLoadContext;
return getLoadContext();
},
exclude: [
// Spread the defaults but replace the /.css$/ rule so that Bull
+12 -28
View File
@@ -1,7 +1,7 @@
###########################
# BASE CONTAINER #
###########################
FROM node:24-alpine3.23 AS base
FROM node:22-alpine3.22 AS base
RUN apk add --no-cache openssl
RUN apk add --no-cache font-freefont
@@ -19,10 +19,7 @@ WORKDIR /app
COPY . .
# Install the exact turbo version resolved in the lockfile, without installing
# the rest of the dependency tree (prune must run before any npm ci).
RUN TURBO_VERSION="$(jq -r '.packages["node_modules/turbo"].version' package-lock.json)" \
&& npm install -g "turbo@${TURBO_VERSION}"
RUN npm install -g "turbo@^2.10.0"
# Outputs to the /out folder
# source: https://turbo.build/repo/docs/reference/command-line-reference/prune#--docker
@@ -42,9 +39,9 @@ RUN apk add --no-cache make cmake g++ openssl bash
WORKDIR /app
# Disable husky from installing hooks
ENV HUSKY=0
ENV DOCKER_OUTPUT=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV HUSKY 0
ENV DOCKER_OUTPUT 1
ENV NEXT_TELEMETRY_DISABLED 1
# Encryption keys
ARG NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE"
@@ -79,7 +76,6 @@ COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/package-lock.json ./package-lock.json
COPY --from=builder /app/lingui.config.ts ./lingui.config.ts
COPY --from=builder /app/patches ./patches
RUN npm ci
@@ -88,17 +84,17 @@ COPY --from=builder /app/out/full/ .
# Finally copy the turbo.json file so that we can run turbo commands
COPY turbo.json turbo.json
ENV NODE_OPTIONS="--max-old-space-size=8192"
RUN npm install -g "turbo@^2.10.0"
RUN npx turbo run build --filter=@documenso/remix...
RUN turbo run build --filter=@documenso/remix...
###########################
# RUNNER CONTAINER #
###########################
FROM base AS runner
ENV HUSKY=0
ENV DOCKER_OUTPUT=1
ENV HUSKY 0
ENV DOCKER_OUTPUT 1
# Telemetry credentials (baked into image at build time, can be disabled at runtime)
ARG NEXT_PRIVATE_TELEMETRY_KEY=""
@@ -118,19 +114,8 @@ WORKDIR /app
COPY --from=builder --chown=nodejs:nodejs /app/out/json/ .
# Copy the tailwind config files across
COPY --from=builder --chown=nodejs:nodejs /app/out/full/packages/tailwind-config ./packages/tailwind-config
# Copy the patches across
COPY --from=builder --chown=nodejs:nodejs /app/patches ./patches
RUN npm ci --omit=dev --no-audit --no-fund && npm cache clean --force
# Strip build-time residue that ships as production dependencies but is never
# executed at runtime.
RUN rm -rf \
node_modules/react-email/dist/cli \
node_modules/esbuild \
node_modules/@esbuild \
node_modules/.bin/esbuild \
node_modules/.bin/email
RUN npm ci --only=production
# Automatically leverage output traces to reduce image size
# https://nodejs.org/docs/advanced-features/output-file-tracing
@@ -141,9 +126,8 @@ COPY --from=installer --chown=nodejs:nodejs /app/apps/remix/public ./apps/remix/
COPY --from=installer --chown=nodejs:nodejs /app/packages/prisma/schema.prisma ./packages/prisma/schema.prisma
COPY --from=installer --chown=nodejs:nodejs /app/packages/prisma/migrations ./packages/prisma/migrations
# Generate the prisma client again, this time only targeting the client generator
RUN npx prisma generate --schema ./packages/prisma/schema.prisma --generator client \
&& npm cache clean --force
# Generate the prisma client again
RUN npx prisma generate --schema ./packages/prisma/schema.prisma
# Get the start script from docker/
+14 -35
View File
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "2.18.0",
"version": "2.16.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "2.18.0",
"version": "2.16.0",
"hasInstallScript": true,
"workspaces": [
"apps/*",
@@ -15,7 +15,7 @@
"dependencies": {
"@ai-sdk/google-vertex": "5.0.48",
"@documenso/prisma": "*",
"@libpdf/core": "^0.4.2",
"@libpdf/core": "^0.4.1",
"@lingui/conf": "^5.6.0",
"@lingui/core": "^5.6.0",
"@marsidev/react-turnstile": "^1.5.0",
@@ -102,8 +102,8 @@
"zod-prisma-types": "3.3.5"
},
"engines": {
"node": ">=24.0.0",
"npm": ">=11.17.0"
"node": ">=22.0.0",
"npm": ">=11.11.0"
}
},
"apps/docs": {
@@ -416,7 +416,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "2.18.0",
"version": "2.16.0",
"dependencies": {
"@cantoo/pdf-lib": "^2.5.3",
"@documenso/api": "*",
@@ -3153,7 +3153,6 @@
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz",
"integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "10.5.0",
@@ -3165,7 +3164,6 @@
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz",
"integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "10.5.0",
@@ -3176,14 +3174,12 @@
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz",
"integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@chevrotain/utils": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz",
"integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@commitlint/cli": {
@@ -4806,9 +4802,9 @@
"license": "MIT"
},
"node_modules/@libpdf/core": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.4.2.tgz",
"integrity": "sha512-lbkIqLDZCCxjLpiC+8/Xvaru/ME7iVVoihl9tLqbp/CDUWZNF0q3u7s2tBJB9wRW/SzUWID6YPFvBWws770hrQ==",
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.4.1.tgz",
"integrity": "sha512-DWGxWw1na8oFPixz+b6kOgu4tMD8xJLDgyPGVUNqIswO5POHAxsqmTvUvDW0IrwHP7kFRHBV3I3T/KhTABf9rA==",
"license": "MIT",
"dependencies": {
"@noble/ciphers": "^2.2.0",
@@ -5727,7 +5723,6 @@
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz",
"integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chevrotain": "^10.5.0",
@@ -10300,14 +10295,12 @@
"version": "6.19.3",
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.19.3.tgz",
"integrity": "sha512-+D6v7RIF21bJrZAXiiIdW0qR73TleYVCqTDozokdEHdGeqN997O7jJW5z+43s5CVwWTZJIwFGpoeXxDDyXDVxA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/driver-adapter-utils": {
"version": "6.19.3",
"resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-6.19.3.tgz",
"integrity": "sha512-UUxn6VLfKKVqm5n9vexOQgFJ9TCBIxupb7F5FzLQ6iM2VV0WZxhgzbQVqBDsGY+VmQLmqaSoKRusEfy3O5KZpA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.19.3"
@@ -10356,14 +10349,12 @@
"version": "6.19.3",
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.19.3.tgz",
"integrity": "sha512-rzHJaIZEnEDUWNjjFAimsyMCS9osPxwbwiAbymwFprSHJSAglMxMDVU+xbQUCLeDsjUF09W5ag/aBPL2t3YqgA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/generator-helper": {
"version": "6.19.3",
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.19.3.tgz",
"integrity": "sha512-13S8ngSWVKcyuRFqaK/JGMyovjQC5sOKJ9A+ufqePQWeIUbvKO2QY2CmhcV4JAa2vuN22//4Sip5mORVKwS0sw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.19.3",
@@ -10384,7 +10375,6 @@
"version": "6.19.3",
"resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-6.19.3.tgz",
"integrity": "sha512-1V5ba+BNtGFFlgoA8ecyAbjmoMWzIrEuebmLbJpeS7qbhnY6vbc1OZ6qc55lI/VAkfdALSL5vePG5KF9P8feLw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.19.3",
@@ -10415,21 +10405,18 @@
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
"integrity": "sha512-/5JrsJQAOIWSl+9WMM/0ugd737kT9zdMNr7EFaP7ogeMxAhxQ78uaOV6JYGEocD8LC0gZx7MXmVE/CTfYdJ2iA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/schema-engine-wasm": {
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"resolved": "https://registry.npmjs.org/@prisma/schema-engine-wasm/-/schema-engine-wasm-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
"integrity": "sha512-mXtzeePkSdqpr/HAIclqzQEf+tBzLawy7NQ9AaRCfg8N/cruavpsocgbSwOOfV7JTk9JubyzKuR3tp2bNNuWbQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/schema-files-loader": {
"version": "6.19.3",
"resolved": "https://registry.npmjs.org/@prisma/schema-files-loader/-/schema-files-loader-6.19.3.tgz",
"integrity": "sha512-rnoL2PgopghakRZLCZwj+d7LPvjbY9mFholFWuibEKzwO7aqS16s60Hz4wxAnKwqOcS/M6pV7QLrsHBYnNR0KA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/prisma-schema-wasm": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
@@ -16287,7 +16274,6 @@
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz",
"integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "10.5.0",
@@ -16639,7 +16625,6 @@
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==",
"dev": true,
"license": "MIT"
},
"node_modules/collapse-white-space": {
@@ -19886,7 +19871,6 @@
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
@@ -22553,7 +22537,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -26400,7 +26383,6 @@
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prisma-json-types-generator/-/prisma-json-types-generator-3.6.2.tgz",
"integrity": "sha512-WX/oENQ0S74r/Wgd2uuHT5i3KbnwLFCP2Fq5ISzrXkus/htOC4uCaQPYuGP2m/wSeKZZCw1RxptTlD+ib7Ht/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@prisma/generator-helper": "^6.16.1",
@@ -26426,7 +26408,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-2.3.0.tgz",
"integrity": "sha512-/+VF2t2DlY+t/27hhyH5ULWxBAAsMBPgOo8Ltq7uXpBz49lUf4XuO1ff9HV0l7J3aDojG+YyczEvY0og9uRqsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mrleebo/prisma-ast": "^0.13.1",
@@ -27627,7 +27608,6 @@
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz",
"integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==",
"dev": true,
"license": "MIT"
},
"node_modules/rehype-raw": {
@@ -31674,7 +31654,6 @@
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/zod-prisma-types/-/zod-prisma-types-3.3.5.tgz",
"integrity": "sha512-PDuRRCdX1d6ch6UclNvrqM1SLb2qvuaQPvZT/OHRrcxOQqQUnCz009aBc1uxuwMho1+d42TTDCQr+HPXhTSJtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@prisma/dmmf": "^6.16.3",
@@ -32431,17 +32410,17 @@
"nanoid": "^5.1.6",
"prisma": "^6.19.0",
"prisma-extension-kysely": "^3.0.0",
"prisma-json-types-generator": "^3.6.2",
"prisma-kysely": "^2.3.0",
"ts-pattern": "^5.9.0",
"zod": "^3.25.76"
"zod": "^3.25.76",
"zod-prisma-types": "3.3.5"
},
"devDependencies": {
"dotenv": "^17.2.3",
"dotenv-cli": "^11.0.0",
"prisma-json-types-generator": "^3.6.2",
"prisma-kysely": "^2.3.0",
"tsx": "^4.23.1",
"typescript": "5.6.2",
"zod-prisma-types": "3.3.5"
"typescript": "5.6.2"
}
},
"packages/prisma/node_modules/@esbuild/aix-ppc64": {
+5 -5
View File
@@ -5,7 +5,7 @@
"apps/*",
"packages/*"
],
"version": "2.18.0",
"version": "2.16.0",
"scripts": {
"postinstall": "patch-package",
"build": "turbo run build",
@@ -41,10 +41,10 @@
"translate:extract": "lingui extract --clean",
"translate:compile": "lingui compile"
},
"packageManager": "npm@11.19.1",
"packageManager": "npm@11.11.0",
"engines": {
"npm": ">=11.17.0",
"node": ">=24.0.0"
"npm": ">=11.11.0",
"node": ">=22.0.0"
},
"devDependencies": {
"@biomejs/biome": "2.4.8",
@@ -91,7 +91,7 @@
"dependencies": {
"@ai-sdk/google-vertex": "5.0.48",
"@documenso/prisma": "*",
"@libpdf/core": "^0.4.2",
"@libpdf/core": "^0.4.1",
"@lingui/conf": "^5.6.0",
"@lingui/core": "^5.6.0",
"@prisma/extension-read-replicas": "^0.4.1",
@@ -10,7 +10,7 @@ test.describe.configure({ mode: 'parallel' });
const nanoid = customAlphabet('1234567890abcdef', 10);
const ADMIN_PROMPT_PLACEHOLDER = 'Search documents, users, organizations…';
const ADMIN_PROMPT_PLACEHOLDER = 'Search documents, users, organisations…';
test('[ADMIN][GLOBAL_SEARCH]: numeric query shows verified user result and navigates', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
@@ -437,98 +437,3 @@ test('[ADMIN][GLOBAL_SEARCH]: over-length query skips the admin search without e
expect(adminSearchRequests).toHaveLength(0);
});
test('[ADMIN][GLOBAL_SEARCH]: capped recipients group links to the admin documents page', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
const { user: sender, team } = await seedUser();
const recipientPrefix = `viewall-recipient-${nanoid()}`;
// Seed 5 documents, each with one recipient email sharing the prefix, to
// hit the 5 result cap on the recipients group.
const documents = [];
for (let i = 0; i < 5; i++) {
documents.push(
await seedPendingDocument(sender, team.id, [`${recipientPrefix}-${i}@test.documenso.com`], {
createDocumentOptions: { title: `recipient-viewall-${nanoid()}` },
}),
);
}
await apiSignin({ page, email: adminUser.email });
await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);
await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(recipientPrefix);
await expect(page.getByText('Global Recipients', { exact: true })).toBeVisible();
// Only the recipients group matches the prefix, so this is its link.
const viewAllOption = page.getByRole('option').filter({ hasText: 'View all results' }).first();
await expect(viewAllOption.getByRole('link')).toHaveAttribute(
'href',
`/admin/documents?term=${encodeURIComponent(`recipient:${recipientPrefix}`)}`,
);
await viewAllOption.click();
await page.waitForURL((url) => url.pathname === '/admin/documents');
// The term input is prefilled with the recipient query and the matching
// documents are listed.
await expect(page.getByPlaceholder(/Search by document title/)).toHaveValue(`recipient:${recipientPrefix}`);
for (const document of documents) {
await expect(page.getByRole('link', { name: document.title })).toBeVisible();
}
});
test('[ADMIN][GLOBAL_SEARCH]: view all results updates the documents page when already on it', async ({ page }) => {
const { user: adminUser } = await seedUser({ isAdmin: true });
const { user: sender, team } = await seedUser();
const recipientPrefix = `viewall-live-${nanoid()}`;
// Seed 5 documents with recipients matching the prefix to hit the cap, and
// one control document whose recipient does not match: with a stale
// (unfiltered) query the control would show, with the filter it must not.
const documents = [];
for (let i = 0; i < 5; i++) {
documents.push(
await seedPendingDocument(sender, team.id, [`${recipientPrefix}-${i}@test.documenso.com`], {
createDocumentOptions: { title: `recipient-live-${nanoid()}` },
}),
);
}
const controlDocument = await seedPendingDocument(sender, team.id, [`control-${nanoid()}@test.documenso.com`], {
createDocumentOptions: { title: `recipient-live-control-${nanoid()}` },
});
await apiSignin({ page, email: adminUser.email });
// Start ON the admin documents page: the buggy state initializer has
// already run with an empty term.
await page.goto('/admin/documents');
await expect(page.getByPlaceholder(/Search by document title/)).toBeVisible();
await openCommandMenu(page, ADMIN_PROMPT_PLACEHOLDER);
await page.getByPlaceholder(ADMIN_PROMPT_PLACEHOLDER).first().fill(recipientPrefix);
await expect(page.getByText('Global Recipients', { exact: true })).toBeVisible();
await page.getByRole('option').filter({ hasText: 'View all results' }).first().click();
await page.waitForURL((url) => url.searchParams.get('term') === `recipient:${recipientPrefix}`);
// The same-route navigation must update both the input and the results.
await expect(page.getByPlaceholder(/Search by document title/)).toHaveValue(`recipient:${recipientPrefix}`);
await expect(page.getByRole('link', { name: documents[0].title })).toBeVisible();
await expect(page.getByRole('link', { name: controlDocument.title })).toHaveCount(0);
});
@@ -411,7 +411,7 @@ test('[ADMIN][DELETE_ORG]: the original owner loses access after deletion', asyn
});
// They should NOT see the organisation settings heading for this org.
await expect(page.getByText('Organization Settings')).not.toBeVisible();
await expect(page.getByText('Organisation Settings')).not.toBeVisible();
});
// ─── Access control: UI ──────────────────────────────────────────────────────
@@ -50,9 +50,9 @@ test('[ADMIN]: promote member to owner', async ({ page }) => {
});
// Verify we're on the admin organisation page
await expect(page.getByText(`Manage organization`)).toBeVisible();
await expect(page.getByText(`Manage organisation`)).toBeVisible();
await expect(page.getByLabel('Organization Name')).toHaveValue(organisation.name);
await expect(page.getByLabel('Organisation Name')).toHaveValue(organisation.name);
// Check that the organisation members table shows the correct roles
const ownerRow = page.getByRole('row', { name: ownerUser.email });
@@ -356,7 +356,7 @@ test('[ADMIN]: error handling for invalid organisation', async ({ page }) => {
});
// Should show 404 error
await expect(page.getByRole('heading', { name: 'Organization not found' })).toBeVisible({
await expect(page.getByRole('heading', { name: 'Organisation not found' })).toBeVisible({
timeout: 10_000,
});
});
@@ -525,8 +525,8 @@ test('[ADMIN]: verify organisation access after ownership change', async ({ page
// Should be able to access organisation settings
await expect(page.getByTestId('unified-settings-sidebar')).toBeVisible();
await expect(page.getByLabel('Organization Name*')).toBeVisible();
await expect(page.getByLabel('Organization Name*')).toBeEnabled();
await expect(page.getByLabel('Organisation Name*')).toBeVisible();
await expect(page.getByLabel('Organisation Name*')).toBeEnabled();
// Should have delete permissions
await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible();

Some files were not shown because too many files have changed in this diff Show More