Compare commits

..

2 Commits

38 changed files with 1109 additions and 627 deletions

View File

@ -22,6 +22,15 @@ Documenso supports Webhooks and allows you to subscribe to the following events:
- `document.completed`
- `document.rejected`
- `document.cancelled`
- `document.viewed`
- `document.recipient.completed`
- `document.downloaded`
- `document.reminder.sent`
- `template.created`
- `template.updated`
- `template.deleted`
- `template.used`
- `recipient.authentication.failed`
## Create a webhook subscription
@ -38,7 +47,7 @@ Clicking on the "**Create Webhook**" button opens a modal to create a new webhoo
To create a new webhook subscription, you need to provide the following information:
- Enter the webhook URL that will receive the event payload.
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`.
- Select the event(s) you want to subscribe to: `document.created`, `document.sent`, `document.opened`, `document.signed`, `document.completed`, `document.rejected`, `document.cancelled`, `document.viewed`, `document.recipient.completed`, `document.downloaded`, `document.reminder.sent`, `template.created`, `template.updated`, `template.deleted`, `template.used`, `recipient.authentication.failed`.
- Optionally, you can provide a secret key that will be used to sign the payload. This key will be included in the `X-Documenso-Secret` header of the request.
![A screenshot of the Create Webhook modal that shows the URL input field and the event checkboxes](/webhook-images/webhooks-page-create-webhook-modal.webp)
@ -619,6 +628,591 @@ Example payload for the `document.rejected` event:
}
```
Example payload for the `document.viewed` event:
```json
{
"event": "DOCUMENT_VIEWED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "John Doe",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:50:26.174Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `document.recipient.completed` event:
```json
{
"event": "DOCUMENT_RECIPIENT_COMPLETED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:51:10.055Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 50,
"documentId": 10,
"templateId": null,
"email": "signer1@documenso.com",
"name": "Signer 1",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:51:10.055Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "SIGNED",
"sendStatus": "SENT"
},
{
"id": 51,
"documentId": 10,
"templateId": null,
"email": "signer2@documenso.com",
"name": "Signer 2",
"token": "HkrptwS42ZBXdRKj1TyUo",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 2,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:51:10.577Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `document.downloaded` event:
```json
{
"event": "DOCUMENT_DOWNLOADED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "COMPLETED",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:52:05.708Z",
"completedAt": "2024-04-22T11:52:05.707Z",
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 51,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "HkrptwS42ZBXdRKj1TyUo",
"documentDeletedAt": null,
"expired": null,
"signedAt": "2024-04-22T11:52:05.688Z",
"authOptions": {
"accessAuth": null,
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:53:18.577Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `document.reminder.sent` event:
```json
{
"event": "DOCUMENT_REMINDER_SENT",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T12:00:00.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.created` event:
```json
{
"event": "TEMPLATE_CREATED",
"payload": {
"id": 5,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract.pdf",
"status": "DRAFT",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:44:43.341Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_456",
"subject": "Employment Contract",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 25,
"documentId": null,
"templateId": 5,
"email": "employee@company.com",
"name": "Employee",
"token": "TemplateToken123",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T11:44:44.779Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.updated` event:
```json
{
"event": "TEMPLATE_UPDATED",
"payload": {
"id": 5,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract_v2.pdf",
"status": "DRAFT",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T12:30:00.000Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_456",
"subject": "Employment Contract - Updated",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 25,
"documentId": null,
"templateId": 5,
"email": "employee@company.com",
"name": "Employee",
"token": "TemplateToken123",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T12:30:01.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.deleted` event:
```json
{
"event": "TEMPLATE_DELETED",
"payload": {
"id": 5,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract.pdf",
"status": "DRAFT",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:44:43.341Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_456",
"subject": "Employment Contract",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 25,
"documentId": null,
"templateId": 5,
"email": "employee@company.com",
"name": "Employee",
"token": "TemplateToken123",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T13:00:00.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `template.used` event:
```json
{
"event": "TEMPLATE_USED",
"payload": {
"id": 15,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "employment_contract.pdf",
"status": "DRAFT",
"documentDataId": "new_doc_data_123",
"createdAt": "2024-04-22T14:00:00.000Z",
"updatedAt": "2024-04-22T14:00:00.000Z",
"completedAt": null,
"deletedAt": null,
"teamId": 2,
"templateId": 5,
"source": "TEMPLATE",
"documentMeta": {
"id": "doc_meta_789",
"subject": "Employment Contract",
"message": "Please review and sign your employment contract.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 60,
"documentId": 15,
"templateId": 5,
"email": "newemployee@company.com",
"name": "New Employee",
"token": "DocToken456",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": null,
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "NOT_SENT"
}
]
},
"createdAt": "2024-04-22T14:00:01.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
Example payload for the `recipient.authentication.failed` event:
```json
{
"event": "RECIPIENT_AUTHENTICATION_FAILED",
"payload": {
"id": 10,
"externalId": null,
"userId": 1,
"authOptions": null,
"formValues": null,
"visibility": "EVERYONE",
"title": "documenso.pdf",
"status": "PENDING",
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
"createdAt": "2024-04-22T11:44:43.341Z",
"updatedAt": "2024-04-22T11:48:07.569Z",
"completedAt": null,
"deletedAt": null,
"teamId": null,
"templateId": null,
"source": "DOCUMENT",
"documentMeta": {
"id": "doc_meta_123",
"subject": "Please sign this document",
"message": "Hello, please review and sign this document.",
"timezone": "UTC",
"password": null,
"dateFormat": "MM/DD/YYYY",
"redirectUrl": null,
"signingOrder": "PARALLEL",
"typedSignatureEnabled": true,
"language": "en",
"distributionMethod": "EMAIL",
"emailSettings": null
},
"Recipient": [
{
"id": 52,
"documentId": 10,
"templateId": null,
"email": "signer@documenso.com",
"name": "Signer",
"token": "vbT8hi3jKQmrFP_LN1WcS",
"documentDeletedAt": null,
"expired": null,
"signedAt": null,
"authOptions": {
"accessAuth": "TWO_FACTOR_AUTH",
"actionAuth": null
},
"signingOrder": 1,
"rejectionReason": null,
"role": "SIGNER",
"readStatus": "NOT_OPENED",
"signingStatus": "NOT_SIGNED",
"sendStatus": "SENT"
}
]
},
"createdAt": "2024-04-22T11:49:00.000Z",
"webhookEndpoint": "https://mywebhooksite.com/mywebhook"
}
```
## Webhook Events Testing
You can trigger test webhook events to test the webhook functionality. To trigger a test webhook, navigate to the [Webhooks page](/developers/webhooks) and click on the "Test Webhook" button.

View File

@ -152,18 +152,6 @@ export const EditorFieldTextForm = ({
className="h-auto"
placeholder={t`Add text to the field`}
{...field}
onChange={(e) => {
const values = form.getValues();
const characterLimit = values.characterLimit || 0;
let textValue = e.target.value;
if (characterLimit > 0 && textValue.length > characterLimit) {
textValue = textValue.slice(0, characterLimit);
}
e.target.value = textValue;
field.onChange(e);
}}
rows={1}
/>
</FormControl>
@ -187,18 +175,6 @@ export const EditorFieldTextForm = ({
className="bg-background"
placeholder={t`Field character limit`}
{...field}
onChange={(e) => {
field.onChange(e);
const values = form.getValues();
const characterLimit = parseInt(e.target.value, 10) || 0;
const textValue = values.text || '';
if (characterLimit > 0 && textValue.length > characterLimit) {
form.setValue('text', textValue.slice(0, characterLimit));
}
}}
/>
</FormControl>
<FormMessage />

View File

@ -89,10 +89,7 @@ export const DirectTemplatePageView = ({
setStep('sign');
};
const onSignDirectTemplateSubmit = async (
fields: DirectTemplateLocalField[],
nextSigner?: { name: string; email: string },
) => {
const onSignDirectTemplateSubmit = async (fields: DirectTemplateLocalField[]) => {
try {
let directTemplateExternalId = searchParams?.get('externalId') || undefined;
@ -101,7 +98,6 @@ export const DirectTemplatePageView = ({
}
const { token } = await createDocumentFromDirectTemplate({
nextSigner,
directTemplateToken,
directTemplateExternalId,
directRecipientName: fullName,

View File

@ -55,13 +55,10 @@ import { DocumentSigningRecipientProvider } from '../document-signing/document-s
export type DirectTemplateSigningFormProps = {
flowStep: DocumentFlowStep;
directRecipient: Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token' | 'id'>;
directRecipient: Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token'>;
directRecipientFields: Field[];
template: Omit<TTemplate, 'user'>;
onSubmit: (
_data: DirectTemplateLocalField[],
_nextSigner?: { name: string; email: string },
) => Promise<void>;
onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
};
export type DirectTemplateLocalField = Field & {
@ -152,7 +149,7 @@ export const DirectTemplateSigningForm = ({
validateFieldsInserted(fieldsRequiringValidation);
};
const handleSubmit = async (nextSigner?: { name: string; email: string }) => {
const handleSubmit = async () => {
setValidateUninsertedFields(true);
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
@ -164,7 +161,7 @@ export const DirectTemplateSigningForm = ({
setIsSubmitting(true);
try {
await onSubmit(localFields, nextSigner);
await onSubmit(localFields);
} catch {
setIsSubmitting(false);
}
@ -221,30 +218,6 @@ export const DirectTemplateSigningForm = ({
setLocalFields(updatedFields);
}, []);
const nextRecipient = useMemo(() => {
if (
!template.templateMeta?.signingOrder ||
template.templateMeta.signingOrder !== 'SEQUENTIAL' ||
!template.templateMeta.allowDictateNextSigner
) {
return undefined;
}
const sortedRecipients = template.recipients.sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id;
if (a.signingOrder === null) return 1;
if (b.signingOrder === null) return -1;
if (a.signingOrder === b.signingOrder) return a.id - b.id;
return a.signingOrder - b.signingOrder;
});
const currentIndex = sortedRecipients.findIndex((r) => r.id === directRecipient.id);
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
? sortedRecipients[currentIndex + 1]
: undefined;
}, [template.templateMeta?.signingOrder, template.recipients, directRecipient.id]);
return (
<DocumentSigningRecipientProvider recipient={directRecipient}>
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
@ -444,15 +417,11 @@ export const DirectTemplateSigningForm = ({
<DocumentSigningCompleteDialog
isSubmitting={isSubmitting}
onSignatureComplete={async (nextSigner) => handleSubmit(nextSigner)}
onSignatureComplete={async () => handleSubmit()}
documentTitle={template.title}
fields={localFields}
fieldsValidated={fieldsValidated}
recipient={directRecipient}
allowDictateNextSigner={nextRecipient && template.templateMeta?.allowDictateNextSigner}
defaultNextSigner={
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
}
/>
</div>
</DocumentFlowFormContainerFooter>

View File

@ -9,7 +9,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentSigningAuthPageViewProps = {
email?: string;
email: string;
emailHasAccount?: boolean;
};
@ -22,18 +22,12 @@ export const DocumentSigningAuthPageView = ({
const [isSigningOut, setIsSigningOut] = useState(false);
const handleChangeAccount = async (email?: string) => {
const handleChangeAccount = async (email: string) => {
try {
setIsSigningOut(true);
let redirectPath = '/signin';
if (email) {
redirectPath = emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`;
}
await authClient.signOut({
redirectPath,
redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
});
} catch {
toast({
@ -55,13 +49,9 @@ export const DocumentSigningAuthPageView = ({
</h1>
<p className="text-muted-foreground mt-2 text-sm">
{email ? (
<Trans>
You need to be logged in as <strong>{email}</strong> to view this page.
</Trans>
) : (
<Trans>You need to be logged in to view this page.</Trans>
)}
<Trans>
You need to be logged in as <strong>{email}</strong> to view this page.
</Trans>
</p>
<Button

View File

@ -24,10 +24,7 @@ type PasskeyData = {
isError: boolean;
};
type SigningAuthRecipient = Pick<
Recipient,
'authOptions' | 'email' | 'role' | 'name' | 'token' | 'id'
>;
type SigningAuthRecipient = Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token'>;
export type DocumentSigningAuthContextValue = {
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;

View File

@ -304,6 +304,7 @@ export const DocumentSigningCompleteDialog = ({
<form onSubmit={form.handleSubmit(onFormSubmit)}>
{allowDictateNextSigner && defaultNextSigner && (
<div className="mb-4 flex flex-col gap-4">
{/* Todo: Envelopes - Should we say "The next recipient to sign this document will be"? */}
<div className="flex flex-col gap-4 md:flex-row">
<FormField
control={form.control}

View File

@ -13,7 +13,6 @@ import { prop, sortBy } from 'remeda';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import {
isFieldUnsignedAndRequired,
isRequiredField,
@ -52,11 +51,7 @@ export type EnvelopeSigningContextValue = {
setSelectedAssistantRecipientId: (_value: number | null) => void;
selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null;
signField: (
_fieldId: number,
_value: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => Promise<void>;
signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise<void>;
};
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null);
@ -289,11 +284,9 @@ export const EnvelopeSigningProvider = ({
: null;
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
const signField = async (
fieldId: number,
fieldValue: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => {
const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => {
console.log('insertField', fieldId, fieldValue);
// Set the field locally for direct templates.
if (isDirectTemplate) {
handleDirectTemplateFieldInsertion(fieldId, fieldValue);
@ -304,7 +297,7 @@ export const EnvelopeSigningProvider = ({
token: envelopeData.recipient.token,
fieldId,
fieldValue,
authOptions,
authOptions: undefined,
});
};

View File

@ -103,6 +103,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
fieldUpdates.height = fieldPageHeight;
}
// Todo: envelopes Use id
editorFields.updateFieldByFormId(fieldFormId, fieldUpdates);
// Select the field if it is not already selected.

View File

@ -27,8 +27,7 @@ import type {
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
import { Separator } from '@documenso/ui/primitives/separator';
@ -113,29 +112,7 @@ export const EnvelopeEditorFieldsPage = () => {
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
{/* Document View */}
<div className="mt-4 flex flex-col items-center justify-center">
{envelope.recipients.length === 0 && (
<Alert
variant="neutral"
className="border-border bg-background mb-4 flex max-w-[800px] flex-row items-center justify-between space-y-0 rounded-sm border"
>
<div className="flex flex-col gap-1">
<AlertTitle>
<Trans>Missing Recipients</Trans>
</AlertTitle>
<AlertDescription>
<Trans>You need at least one recipient to add fields</Trans>
</AlertDescription>
</div>
<Button asChild variant="outline">
<Link to={`${relativePath.editorPath}`}>
<Trans>Add Recipients</Trans>
</Link>
</Button>
</Alert>
)}
<div className="mt-4 flex h-full justify-center p-4">
{currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
) : (
@ -153,7 +130,7 @@ export const EnvelopeEditorFieldsPage = () => {
</div>
{/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && envelope.recipients.length > 0 && (
{currentEnvelopeItem && (
<div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4">
{/* Recipient selector section. */}
<section className="px-4">
@ -161,15 +138,29 @@ export const EnvelopeEditorFieldsPage = () => {
<Trans>Selected Recipient</Trans>
</h3>
<RecipientSelector
selectedRecipient={editorFields.selectedRecipient}
onSelectedRecipientChange={(recipient) =>
editorFields.setSelectedRecipient(recipient.id)
}
recipients={envelope.recipients}
className="w-full"
align="end"
/>
{envelope.recipients.length === 0 ? (
<Alert variant="warning">
<AlertDescription className="flex flex-col gap-2">
<Trans>You need at least one recipient to add fields</Trans>
<Link to={`${relativePath.editorPath}`} className="text-sm">
<p>
<Trans>Click here to add a recipient</Trans>
</p>
</Link>
</AlertDescription>
</Alert>
) : (
<RecipientSelector
selectedRecipient={editorFields.selectedRecipient}
onSelectedRecipientChange={(recipient) =>
editorFields.setSelectedRecipient(recipient.id)
}
recipients={envelope.recipients}
className="w-full"
align="end"
/>
)}
{editorFields.selectedRecipient &&
!canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && (

View File

@ -323,7 +323,7 @@ export const EnvelopeEditorSettingsDialog = ({
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
{/* Sidebar. */}
<div className="bg-accent/20 flex w-80 flex-col border-r">
<div className="flex w-80 flex-col border-r bg-gray-50">
<DialogHeader className="p-6 pb-4">
<DialogTitle>Document Settings</DialogTitle>
</DialogHeader>

View File

@ -203,6 +203,7 @@ export const EnvelopeEditorUploadPage = () => {
debouncedUpdateEnvelopeItems(items);
};
// Todo: Envelopes - Sync into envelopes data
const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => {
void updateEnvelopeItems({
envelopeId: envelope.id,

View File

@ -10,17 +10,14 @@ import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-rende
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZFullFieldSchema } from '@documenso/lib/types/field';
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field';
import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field';
@ -31,24 +28,20 @@ import { handleNumberFieldClick } from '~/utils/field-signing/number-field';
import { handleSignatureFieldClick } from '~/utils/field-signing/signature-field';
import { handleTextFieldClick } from '~/utils/field-signing/text-field';
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
export default function EnvelopeSignerPageRenderer() {
const { t, i18n } = useLingui();
const { i18n } = useLingui();
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
const { sessionData } = useOptionalSession();
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { toast } = useToast();
const {
envelopeData,
recipient,
recipientFields,
recipientFieldsRemaining,
showPendingFieldTooltip,
signField: signFieldInternal,
signField,
email,
setEmail,
fullName,
@ -325,6 +318,7 @@ export default function EnvelopeSignerPageRenderer() {
* SIGNATURE FIELD.
*/
.with({ type: FieldType.SIGNATURE }, (field) => {
// Todo: Envelopes - Reauth
handleSignatureFieldClick({
field,
signature,
@ -335,21 +329,11 @@ export default function EnvelopeSignerPageRenderer() {
.then(async (payload) => {
if (payload) {
fieldGroup.add(loadingSpinnerGroup);
await signField(field.id, payload);
}
if (payload.value) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => {
await signField(field.id, payload, authOptions);
loadingSpinnerGroup.destroy();
},
actionTarget: field.type,
});
setSignature(payload.value);
} else {
await signField(field.id, payload);
}
if (payload?.value) {
setSignature(payload.value);
}
})
.finally(() => {
@ -363,26 +347,6 @@ export default function EnvelopeSignerPageRenderer() {
fieldGroup.on('pointerdown', handleFieldGroupClick);
};
const signField = async (
fieldId: number,
payload: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => {
try {
await signFieldInternal(fieldId, payload, authOptions);
} catch (err) {
console.error(err);
toast({
title: t`Error`,
description: t`An error occurred while signing the field.`,
variant: 'destructive',
});
throw err;
}
};
/**
* Initialize the Konva page canvas and all fields and interactions.
*/

View File

@ -127,7 +127,6 @@ export const EnvelopeSignerCompleteDialog = () => {
isBase64,
};
}),
nextSigner,
});
const redirectUrl = envelope.documentMeta.redirectUrl;

View File

@ -8,6 +8,7 @@ import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/env
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
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 { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
@ -97,12 +98,15 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
envelopeForSigning,
} as const;
})
.catch((e) => {
.catch(async (e) => {
const error = AppError.parseError(e);
if (error.code === AppErrorCode.UNAUTHORIZED) {
const requiredAccessData = await getEnvelopeRequiredAccessData({ token });
return {
isDocumentAccessValid: false,
...requiredAccessData,
} as const;
}
@ -222,21 +226,20 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
const user = sessionData?.user;
if (!data.isDocumentAccessValid) {
return <DocumentSigningAuthPageView email={''} emailHasAccount={true} />;
return (
<DocumentSigningAuthPageView
email={data.recipientEmail}
emailHasAccount={!!data.recipientHasAccount}
/>
);
}
const { envelope, recipient } = data.envelopeForSigning;
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
});
const isEmailForced = derivedRecipientAccessAuth.includes(DocumentAccessAuth.ACCOUNT);
return (
<EnvelopeSigningProvider
envelopeData={data.envelopeForSigning}
email={isEmailForced ? user?.email || '' : ''} // Doing this allows us to let users change the email if they want to for non-auth templates.
email={''} // Doing this allows us to let users change the email if they want to.
fullName={user?.name}
signature={user?.signature}
>

View File

@ -78,6 +78,7 @@ test.describe('Signing Certificate Tests', () => {
},
});
// Todo: Envelopes
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
const completedDocumentData = await getFile(firstDocumentData);
@ -168,6 +169,7 @@ test.describe('Signing Certificate Tests', () => {
},
});
// Todo: Envelopes
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
const completedDocumentData = await getFile(firstDocumentData);

View File

@ -1,12 +1,9 @@
import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { customAlphabet } from 'nanoid';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
@ -124,7 +121,7 @@ test('[DIRECT_TEMPLATES]: delete direct template link', async ({ page }) => {
await expect(page.getByText('404 not found')).toBeVisible();
});
test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page }) => {
test('[DIRECT_TEMPLATES]: direct template link auth access', async ({ page }) => {
const { user, team } = await seedUser();
const directTemplateWithAuth = await seedDirectTemplate({
@ -156,53 +153,6 @@ test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page })
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await expect(page.getByLabel('Email')).toBeDisabled();
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
});
test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page }) => {
const { user, team } = await seedUser();
const directTemplateWithAuth = await seedDirectTemplate({
title: 'Personal direct template link',
userId: user.id,
teamId: team.id,
internalVersion: 2,
createTemplateOptions: {
authOptions: createDocumentAuthOptions({
globalAccessAuth: ['ACCOUNT'],
globalActionAuth: [],
}),
},
});
const directTemplatePath = formatDirectTemplatePath(
directTemplateWithAuth.directLink?.token || '',
);
await page.goto(directTemplatePath);
await expect(page.getByText('Authentication required')).toBeVisible();
await apiSignin({
page,
email: user.email,
});
await page.goto(directTemplatePath);
await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByLabel('Your Email')).not.toBeVisible();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
});
test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ page }) => {
@ -225,9 +175,6 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
@ -236,173 +183,3 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
// Add a longer waiting period to ensure document status is updated
await page.waitForTimeout(3000);
});
test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with next signer dictation', async ({
page,
}) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 1,
});
// Should be visible to team members.
const template = await seedDirectTemplate({
title: 'Team direct template link 1',
userId: owner.id,
teamId: team.id,
});
await prisma.documentMeta.update({
where: {
id: template.documentMetaId,
},
data: {
allowDictateNextSigner: true,
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
email: originalSecondSignerEmail,
name: originalName,
token: Math.random().toString().slice(2, 7),
role: RecipientRole.SIGNER,
},
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await page.waitForTimeout(100);
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByText('Next Recipient Name')).toBeVisible();
const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
expect(nextRecipientNameInputValue).toBe(originalName);
const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);
const newName = 'Hello';
const newSecondSignerEmail = seedTestEmail();
await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
await page.getByLabel('Next Recipient Name').fill(newName);
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
const createdEnvelopeRecipients = await prisma.recipient.findMany({
where: {
envelope: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
},
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
});
test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with next signer dictation', async ({
page,
}) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 1,
});
// Should be visible to team members.
const template = await seedDirectTemplate({
title: 'Team direct template link 1',
userId: owner.id,
teamId: team.id,
internalVersion: 2,
});
await prisma.documentMeta.update({
where: {
id: template.documentMetaId,
},
data: {
allowDictateNextSigner: true,
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
email: originalSecondSignerEmail,
name: originalName,
token: Math.random().toString().slice(2, 7),
role: RecipientRole.SIGNER,
},
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'Team direct template link 1' })).toBeVisible();
await page.waitForTimeout(100);
await page.getByRole('button', { name: 'Complete' }).click();
const currentName = 'John Doe';
const currentEmail = seedTestEmail();
await page.getByPlaceholder('Enter Your Name').fill(currentName);
await page.getByPlaceholder('Enter Your Email').fill(currentEmail);
await expect(page.getByText('Next Recipient Name')).toBeVisible();
const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
expect(nextRecipientNameInputValue).toBe(originalName);
const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);
const newName = 'Hello';
const newSecondSignerEmail = seedTestEmail();
await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
await page.getByLabel('Next Recipient Name').fill(newName);
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
const createdEnvelopeRecipients = await prisma.recipient.findMany({
where: {
envelope: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
},
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
});

View File

@ -97,7 +97,9 @@ export const completeDocumentWithToken = async ({
}
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
const isRecipientsTurn = await getIsRecipientsTurnToSign({
token: recipient.token,
});
if (!isRecipientsTurn) {
throw new Error(
@ -151,6 +153,18 @@ export const completeDocumentWithToken = async ({
}),
});
const envelopeForFailure = await prisma.envelope.findUniqueOrThrow({
where: { id: envelope.id },
include: { documentMeta: true, recipients: true },
});
await triggerWebhook({
event: WebhookTriggerEvents.RECIPIENT_AUTHENTICATION_FAILED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeForFailure)),
userId: envelope.userId,
teamId: envelope.teamId,
});
throw new AppError(AppErrorCode.TWO_FACTOR_AUTH_FAILED, {
message: 'Invalid 2FA authentication',
});
@ -205,6 +219,18 @@ export const completeDocumentWithToken = async ({
});
});
const envelopeWithRelations = await prisma.envelope.findUniqueOrThrow({
where: { id: envelope.id },
include: { documentMeta: true, recipients: true },
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeWithRelations)),
userId: envelope.userId,
teamId: envelope.teamId,
});
await jobs.triggerJob({
name: 'send.recipient.signed.email',
payload: {

View File

@ -7,6 +7,7 @@ import {
OrganisationType,
RecipientRole,
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import { mailer } from '@documenso/email/mailer';
@ -24,11 +25,16 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { isDocumentCompleted } from '../../utils/document';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { getEmailContext } from '../email/get-email-context';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type ResendDocumentOptions = {
id: EnvelopeIdOptions;
@ -230,4 +236,11 @@ export const resendDocument = async ({
);
}),
);
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_REMINDER_SENT,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
userId: envelope.userId,
teamId: envelope.teamId,
});
};

View File

@ -1,5 +1,4 @@
import { EnvelopeType, ReadStatus, SendStatus } from '@prisma/client';
import { WebhookTriggerEvents } from '@prisma/client';
import { EnvelopeType, ReadStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
@ -66,6 +65,13 @@ export const viewedDocument = async ({
}),
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_VIEWED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
userId: envelope.userId,
teamId: envelope.teamId,
});
// Early return if already opened.
if (recipient.readStatus === ReadStatus.OPENED) {
return;

View File

@ -386,6 +386,13 @@ export const createEnvelope = async ({
userId,
teamId,
});
} else if (type === EnvelopeType.TEMPLATE) {
await triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_CREATED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
userId,
teamId,
});
}
return createdEnvelope;

View File

@ -1,11 +1,10 @@
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
import { extractDocumentAuthMethods } from '../../utils/document-auth';
import type { TDocumentAuthMethods } from '../../types/document-auth';
import { isRecipientAuthorized } from '../document/is-recipient-authorized';
import { getTeamSettings } from '../team/get-team-settings';
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
@ -99,28 +98,14 @@ export const getEnvelopeForDirectTemplateSigning = async ({
});
}
// Currently not using this since for direct templates "User" access means they just need to be
// logged in.
// const documentAccessValid = await isRecipientAuthorized({
// type: 'ACCESS',
// documentAuthOptions: envelope.authOptions,
// recipient,
// userId,
// authOptions: accessAuth,
// });
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
const documentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
documentAuthOptions: envelope.authOptions,
recipient,
userId,
authOptions: accessAuth,
});
// Ensure typesafety when we add more options.
const documentAccessValid = derivedRecipientAccessAuth.every((auth) =>
match(auth)
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(userId))
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true)
.exhaustive(),
);
if (!documentAccessValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Invalid access values',

View File

@ -54,3 +54,54 @@ export const getEnvelopeRequiredAccessData = async ({ token }: { token: string }
recipientHasAccount: Boolean(recipientUserAccount),
} as const;
};
export const getEnvelopeDirectTemplateRequiredAccessData = async ({ token }: { token: string }) => {
const envelope = await prisma.envelope.findFirst({
where: {
type: EnvelopeType.TEMPLATE,
directLink: {
enabled: true,
token,
},
status: DocumentStatus.DRAFT,
},
include: {
recipients: {
where: {
token,
},
},
directLink: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const recipient = envelope.recipients.find(
(r) => r.id === envelope.directLink?.directTemplateRecipientId,
);
if (!recipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
const recipientUserAccount = await prisma.user.findFirst({
where: {
email: recipient.email.toLowerCase(),
},
select: {
id: true,
},
});
return {
recipientEmail: recipient.email,
recipientHasAccount: Boolean(recipientUserAccount),
} as const;
};

View File

@ -1,6 +1,5 @@
import type { DocumentMeta, DocumentVisibility, Prisma, TemplateType } from '@prisma/client';
import { EnvelopeType, FolderType } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { DocumentStatus, EnvelopeType, FolderType, WebhookTriggerEvents } from '@prisma/client';
import { isDeepEqual } from 'remeda';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
@ -12,9 +11,14 @@ import { prisma } from '@documenso/prisma';
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { getEnvelopeWhereInput } from './get-envelope-by-id';
export type UpdateEnvelopeOptions = {
@ -339,6 +343,22 @@ export const updateEnvelope = async ({
});
}
if (envelope.type === EnvelopeType.TEMPLATE) {
const envelopeWithRelations = await tx.envelope.findUniqueOrThrow({
where: { id: updatedEnvelope.id },
include: { documentMeta: true, recipients: true },
});
void triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_UPDATED,
data: ZWebhookDocumentSchema.parse(
mapEnvelopeToWebhookDocumentPayload(envelopeWithRelations),
),
userId,
teamId,
});
}
return updatedEnvelope;
});
};

View File

@ -3,7 +3,6 @@ import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import type { Field, Signature } from '@prisma/client';
import {
DocumentSigningOrder,
DocumentSource,
DocumentStatus,
EnvelopeType,
@ -27,7 +26,7 @@ import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/f
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import { ZFieldMetaSchema } from '../../types/field-meta';
@ -69,10 +68,6 @@ export type CreateDocumentFromDirectTemplateOptions = {
name?: string;
email: string;
};
nextSigner?: {
email: string;
name: string;
};
};
type CreatedDirectRecipientField = {
@ -97,7 +92,6 @@ export const createDocumentFromDirectTemplate = async ({
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
nextSigner,
requestMetadata,
user,
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
@ -134,17 +128,6 @@ export const createDocumentFromDirectTemplate = async ({
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
}
if (
nextSigner &&
(!directTemplateEnvelope.documentMeta?.allowDictateNextSigner ||
directTemplateEnvelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'You need to enable allowDictateNextSigner and sequential signing to dictate the next signer',
});
}
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(
directTemplateEnvelope.secondaryId,
);
@ -647,77 +630,6 @@ export const createDocumentFromDirectTemplate = async ({
}),
];
if (nextSigner) {
const pendingRecipients = await tx.recipient.findMany({
select: {
id: true,
signingOrder: true,
name: true,
email: true,
role: true,
},
where: {
envelopeId: createdEnvelope.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
role: {
not: RecipientRole.CC,
},
},
// Composite sort so our next recipient is always the one with the lowest signing order or id
// if there is a tie.
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
});
const nextRecipient = pendingRecipients[0];
if (nextRecipient) {
auditLogsToCreate.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
envelopeId: createdEnvelope.id,
user: {
name: user?.name || directRecipientName || '',
email: user?.email || directRecipientEmail,
},
metadata: requestMetadata,
data: {
recipientEmail: nextRecipient.email,
recipientName: nextRecipient.name,
recipientId: nextRecipient.id,
recipientRole: nextRecipient.role,
changes: [
{
type: RECIPIENT_DIFF_TYPE.NAME,
from: nextRecipient.name,
to: nextSigner.name,
},
{
type: RECIPIENT_DIFF_TYPE.EMAIL,
from: nextRecipient.email,
to: nextSigner.email,
},
],
},
}),
);
await tx.recipient.update({
where: { id: nextRecipient.id },
data: {
sendStatus: SendStatus.SENT,
...(nextSigner && documentMeta?.allowDictateNextSigner
? {
name: nextSigner.name,
email: nextSigner.email,
}
: {}),
},
});
}
}
await tx.documentAuditLog.createMany({
data: auditLogsToCreate,
});

View File

@ -725,6 +725,13 @@ export const createDocumentFromTemplate = async ({
teamId,
});
await triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_USED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
userId,
teamId,
});
return envelope;
});
};

View File

@ -1,9 +1,14 @@
import { EnvelopeType } from '@prisma/client';
import { EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { type EnvelopeIdOptions } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type DeleteTemplateOptions = {
id: EnvelopeIdOptions;
@ -19,6 +24,18 @@ export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptio
teamId,
});
const templateToDelete = await prisma.envelope.findUniqueOrThrow({
where: envelopeWhereInput,
include: { documentMeta: true, recipients: true },
});
await triggerWebhook({
event: WebhookTriggerEvents.TEMPLATE_DELETED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(templateToDelete)),
userId,
teamId,
});
return await prisma.envelope.delete({
where: envelopeWhereInput,
});

View File

@ -480,5 +480,198 @@ export const generateSampleWebhookPayload = (
};
}
if (event === WebhookTriggerEvents.DOCUMENT_VIEWED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_RECIPIENT_COMPLETED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_DOWNLOADED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.COMPLETED,
completedAt: now,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
signedAt: now,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.DOCUMENT_REMINDER_SENT) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
Recipient: [
{
...basePayload.recipients[0],
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.RECIPIENT_AUTHENTICATION_FAILED) {
return {
event,
payload: {
...basePayload,
status: DocumentStatus.PENDING,
recipients: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
Recipient: [
{
...basePayload.recipients[0],
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
},
],
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_CREATED) {
return {
event,
payload: {
...basePayload,
title: 'My Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_UPDATED) {
return {
event,
payload: {
...basePayload,
title: 'My Updated Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_DELETED) {
return {
event,
payload: {
...basePayload,
title: 'Deleted Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
if (event === WebhookTriggerEvents.TEMPLATE_USED) {
return {
event,
payload: {
...basePayload,
title: 'Document from Template',
status: DocumentStatus.DRAFT,
templateId: 10,
source: DocumentSource.TEMPLATE,
},
createdAt: now.toISOString(),
webhookEndpoint: webhookUrl,
};
}
throw new Error(`Unsupported event type: ${event}`);
};

View File

@ -62,15 +62,16 @@ export const renderCheckboxFieldElement = (
const rectWidth = fieldRect.width() * groupScaleX;
const rectHeight = fieldRect.height() * groupScaleY;
// Todo: Envelopes - check sorting more than 10
// arr.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
const squares = fieldGroup
.find('.checkbox-square')
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
.sort((a, b) => a.id().localeCompare(b.id()));
const checkmarks = fieldGroup
.find('.checkbox-checkmark')
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
const text = fieldGroup
.find('.checkbox-text')
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
.sort((a, b) => a.id().localeCompare(b.id()));
const text = fieldGroup.find('.checkbox-text').sort((a, b) => a.id().localeCompare(b.id()));
const groupedItems = squares.map((square, i) => ({
squareElement: square,

View File

@ -8,9 +8,9 @@ import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import type { TFieldMetaSchema } from '../../types/field-meta';
import { renderCheckboxFieldElement } from './render-checkbox-field';
import { renderDropdownFieldElement } from './render-dropdown-field';
import { renderGenericTextFieldElement } from './render-generic-text-field';
import { renderRadioFieldElement } from './render-radio-field';
import { renderSignatureFieldElement } from './render-signature-field';
import { renderTextFieldElement } from './render-text-field';
export const MIN_FIELD_HEIGHT_PX = 12;
export const MIN_FIELD_WIDTH_PX = 36;
@ -43,9 +43,9 @@ type RenderFieldOptions = {
*
* @default 'edit'
*
* - `edit` - The field is rendered in editor page.
* - `sign` - The field is rendered for the signing page.
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc.
* - `edit` - The field is rendered in edit mode.
* - `sign` - The field is rendered in sign mode. No interactive elements.
* - `export` - The field is rendered in export mode. No backgrounds, interactive elements, etc.
*/
mode: 'edit' | 'sign' | 'export';
@ -76,21 +76,10 @@ export const renderField = ({
};
return match(field.type)
.with(
FieldType.INITIALS,
FieldType.NAME,
FieldType.EMAIL,
FieldType.DATE,
FieldType.TEXT,
FieldType.NUMBER,
() => renderGenericTextFieldElement(field, options),
)
.with(FieldType.TEXT, () => renderTextFieldElement(field, options))
.with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options))
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
.with(FieldType.FREE_SIGNATURE, () => {
throw new Error('Free signature fields are not supported');
})
.exhaustive();
.otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes
};

View File

@ -12,8 +12,6 @@ import {
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
import { calculateFieldPosition } from './field-renderer';
const DEFAULT_TEXT_ALIGN = 'left';
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
@ -33,8 +31,8 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
// Calculate text positioning based on alignment
const textX = 0;
const textY = 0;
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN;
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle';
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left';
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
const textPadding = 10;
@ -42,33 +40,51 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
// Handle edit mode.
if (mode === 'edit') {
if (textMeta?.text) {
textToRender = textMeta.text;
} else if (textMeta?.label) {
textToRender = fieldTypeName;
textAlign = 'center';
textVerticalAlign = 'middle';
if (textMeta?.label) {
textToRender = textMeta.label;
} else {
// Show field name which is centered for the edit mode if no label/text is avaliable.
textToRender = fieldTypeName;
textAlign = 'center';
} else if (textMeta?.text) {
textToRender = textMeta.text;
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
}
}
}
// Handle sign mode.
if (mode === 'sign' || mode === 'export') {
if (!field.inserted) {
if (textMeta?.text) {
textToRender = textMeta.text;
} else if (textMeta?.label) {
textToRender = textMeta.label;
} else if (mode === 'sign') {
// Only show the field name in sign mode if no text/label is avaliable.
textToRender = fieldTypeName;
textAlign = 'center';
textToRender = fieldTypeName;
textAlign = 'center';
textVerticalAlign = 'middle';
if (textMeta?.label) {
textToRender = textMeta.label;
}
if (textMeta?.text) {
textToRender = textMeta.text;
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
}
}
if (field.inserted) {
textToRender = field.customText;
textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta?.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
}
}
}
@ -90,7 +106,7 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
return fieldText;
};
export const renderGenericTextFieldElement = (
export const renderTextFieldElement = (
field: FieldToRender,
options: RenderFieldElementOptions,
) => {

View File

@ -0,0 +1,17 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_VIEWED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_RECIPIENT_COMPLETED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_DOWNLOADED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'DOCUMENT_REMINDER_SENT';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_CREATED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_UPDATED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_DELETED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'TEMPLATE_USED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'RECIPIENT_AUTHENTICATION_FAILED';

View File

@ -172,6 +172,15 @@ enum WebhookTriggerEvents {
DOCUMENT_COMPLETED
DOCUMENT_REJECTED
DOCUMENT_CANCELLED
DOCUMENT_VIEWED
DOCUMENT_RECIPIENT_COMPLETED
DOCUMENT_DOWNLOADED
DOCUMENT_REMINDER_SENT
TEMPLATE_CREATED
TEMPLATE_UPDATED
TEMPLATE_DELETED
TEMPLATE_USED
RECIPIENT_AUTHENTICATION_FAILED
}
model Webhook {

View File

@ -28,7 +28,6 @@ type SeedTemplateOptions = {
title?: string;
userId: number;
teamId: number;
internalVersion?: 1 | 2;
createTemplateOptions?: Partial<Prisma.EnvelopeUncheckedCreateInput>;
};
@ -168,7 +167,7 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
data: {
id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId,
internalVersion: options.internalVersion ?? 1,
internalVersion: 1,
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: {
@ -185,7 +184,6 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
teamId,
recipients: {
create: {
signingOrder: 1,
email: DIRECT_TEMPLATE_RECIPIENT_EMAIL,
name: DIRECT_TEMPLATE_RECIPIENT_NAME,
token: Math.random().toString().slice(2, 7),

View File

@ -1,8 +1,13 @@
import type { DocumentData } from '@prisma/client';
import { DocumentDataType, EnvelopeType } from '@prisma/client';
import { DocumentDataType, EnvelopeType, WebhookTriggerEvents } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { triggerWebhook } from '@documenso/lib/server-only/webhooks/trigger/trigger-webhook';
import {
ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload,
} from '@documenso/lib/types/webhook-payload';
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
@ -76,6 +81,13 @@ export const downloadDocumentRoute = authenticatedProcedure
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;
void triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_DOWNLOADED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
userId: envelope.userId,
teamId: envelope.teamId,
});
return {
downloadUrl: url,
filename,

View File

@ -133,49 +133,6 @@ export const signEnvelopeFieldRoute = procedure
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
// Early return for uninserting fields.
if (!insertionValues.inserted) {
return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
id: field.id,
},
data: {
customText: '',
inserted: false,
},
});
await tx.signature.deleteMany({
where: {
fieldId: field.id,
},
});
if (recipient.role !== RecipientRole.ASSISTANT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
envelopeId: envelope.id,
user: {
name: recipient.name,
email: recipient.email,
},
requestMetadata: metadata.requestMetadata,
data: {
field: field.type,
fieldId: field.secondaryId,
},
}),
});
}
return {
signedField: updatedField,
};
});
}
const derivedRecipientActionAuth = await validateFieldAuth({
documentAuthOptions: envelope.authOptions,
recipient,

View File

@ -519,7 +519,6 @@ export const templateRouter = router({
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
nextSigner,
} = input;
ctx.logger.info({
@ -542,7 +541,6 @@ export const templateRouter = router({
email: ctx.user.email,
}
: undefined,
nextSigner,
requestMetadata: ctx.metadata,
});
}),

View File

@ -90,12 +90,6 @@ export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
directTemplateExternalId: z.string().optional(),
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
templateUpdatedAt: z.date(),
nextSigner: z
.object({
email: z.string().email().max(254),
name: z.string().min(1).max(255),
})
.optional(),
});
export const ZCreateDocumentFromTemplateRequestSchema = z.object({