Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,51 +102,64 @@ import {
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { Switch } from '@documenso/ui/primitives/switch';
import { Textarea } from '@documenso/ui/primitives/textarea';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast';

import { useCurrentTeam } from '~/providers/team';

export const ZAddSettingsFormSchema = z.object({
templateType: z.nativeEnum(TemplateType).optional(),
externalId: z.string().optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
.transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val))
.optional()
.default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
meta: z.object({
subject: z.string(),
message: z.string(),
timezone: ZDocumentMetaTimezoneSchema.default(DEFAULT_DOCUMENT_TIME_ZONE),
dateFormat: ZDocumentMetaDateFormatSchema.default(DEFAULT_DOCUMENT_DATE_FORMAT),
distributionMethod: z
.nativeEnum(DocumentDistributionMethod)
export const ZAddSettingsFormSchema = z
.object({
templateType: z.nativeEnum(TemplateType).optional(),
externalId: z.string().optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
.transform((val) => (val.length === 1 && val[0] === '-1' ? [] : val))
.optional()
.default(DocumentDistributionMethod.EMAIL),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
.default([]),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
meta: z.object({
subject: z.string(),
message: z.string(),
timezone: ZDocumentMetaTimezoneSchema.default(DEFAULT_DOCUMENT_TIME_ZONE),
dateFormat: ZDocumentMetaDateFormatSchema.default(DEFAULT_DOCUMENT_DATE_FORMAT),
distributionMethod: z
.nativeEnum(DocumentDistributionMethod)
.optional()
.default(DocumentDistributionMethod.EMAIL),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
emailId: z.string().nullable(),
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
emailSettings: ZDocumentEmailSettingsSchema,
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
}),
language: z
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
emailId: z.string().nullable(),
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
emailSettings: ZDocumentEmailSettingsSchema,
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
reminderEnabled: z.boolean().default(false),
reminderIntervalDays: z.number().int().min(1).max(30).optional(),
}),
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
}),
});
})
.superRefine((data, ctx) => {
if (data.meta.reminderEnabled && !data.meta.reminderIntervalDays) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: msg`Reminder interval is required when reminders are enabled`.id,
path: ['meta', 'reminderIntervalDays'],
});
}
});

type EnvelopeEditorSettingsTabType = 'general' | 'email' | 'security';

Expand Down Expand Up @@ -222,6 +235,8 @@ export const EnvelopeEditorSettingsDialog = ({
emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings),
signatureTypes: extractTeamSignatureSettings(envelope.documentMeta),
envelopeExpirationPeriod: envelope.documentMeta?.envelopeExpirationPeriod ?? null,
reminderEnabled: envelope.documentMeta?.reminderEnabled ?? false,
reminderIntervalDays: envelope.documentMeta?.reminderIntervalDays ?? undefined,
},
};
};
Expand All @@ -239,6 +254,7 @@ export const EnvelopeEditorSettingsDialog = ({
);

const emailSettings = form.watch('meta.emailSettings');
const envelopeExpirationPeriod = form.watch('meta.envelopeExpirationPeriod');

const { data: emailData, isLoading: isLoadingEmails } =
trpc.enterprise.organisation.email.find.useQuery(
Expand Down Expand Up @@ -270,6 +286,8 @@ export const EnvelopeEditorSettingsDialog = ({
subject,
emailReplyTo,
envelopeExpirationPeriod,
reminderEnabled,
reminderIntervalDays,
} = data.meta;

const parsedGlobalAccessAuth = z
Expand Down Expand Up @@ -300,6 +318,8 @@ export const EnvelopeEditorSettingsDialog = ({
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
envelopeExpirationPeriod,
reminderEnabled,
reminderIntervalDays,
},
});

Expand Down Expand Up @@ -749,6 +769,82 @@ export const EnvelopeEditorSettingsDialog = ({
)}
/>
)}

<FormField
control={form.control}
name="meta.reminderEnabled"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Automatic Reminders</Trans>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>

<TooltipContent className="max-w-xs text-muted-foreground">
{envelopeExpirationPeriod ? (
<Trans>
Automatically send reminder emails to unsigned recipients on a
recurring interval until the document expires.
</Trans>
) : (
<Trans>
Set an expiration date to enable automatic reminders.
</Trans>
)}
</TooltipContent>
</Tooltip>
</FormLabel>

<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={envelopeHasBeenSent || !envelopeExpirationPeriod}
/>
</FormControl>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<FormMessage />
</FormItem>
)}
/>

{form.watch('meta.reminderEnabled') && (
<FormField
control={form.control}
name="meta.reminderIntervalDays"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans>Reminder Interval (days)</Trans>
</FormLabel>

<FormControl>
<Input
type="number"
min={1}
max={30}
className="bg-background"
disabled={envelopeHasBeenSent}
value={field.value ?? ''}
onChange={(e) => {
const raw = e.target.value;
if (raw === '') {
field.onChange(undefined);
return;
}
const parsed = parseInt(raw, 10);
field.onChange(isNaN(parsed) ? undefined : parsed);
}}
/>
</FormControl>

<FormMessage />
</FormItem>
)}
/>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</>
))
.with(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps)
drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled ?? undefined,
dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined,
language: envelope.documentMeta.language as SupportedLanguageCodes,
reminderIntervalDays: envelope.documentMeta.reminderIntervalDays ?? undefined,
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ const EnvelopeEditPage = ({ embedAuthoringOptions }: EnvelopeEditPageProps) => {
drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled, //
dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined,
language: envelope.documentMeta.language as SupportedLanguageCodes,
reminderIntervalDays: envelope.documentMeta.reminderIntervalDays ?? undefined,
},
};

Expand Down
4 changes: 2 additions & 2 deletions packages/lib/types/document-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const ZDocumentEmailSettingsSchema = z
.describe(
'Whether to send a digest email to the document owner when reminders are sent to unsigned recipients. Aggregates all pending documents for the team into a single email.',
)
.default(true),
.default(false),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Default flip likely breaks “opt-out” semantics and legacy behavior

Line 75 and Line 118 switch ownerReminderDigest to default false, so any document with missing/partial/invalid emailSettings now silently disables digest emails. That is a backward-incompatible behavior shift and appears to conflict with the PR objective wording (“allow senders to opt out”).

Suggested fix (preserve opt-out behavior)
-      .default(false),
+      .default(true),
...
-  ownerReminderDigest: false,
+  ownerReminderDigest: true,

Also applies to: 118-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/types/document-email.ts` at line 75, The change flips the
default of ownerReminderDigest to false, which breaks opt-out semantics; instead
restore the legacy default of true for ownerReminderDigest in the email settings
schema so that missing/partial/invalid emailSettings continue to enable digests
unless explicitly set to false (update the default for ownerReminderDigest
wherever it’s defined in the document-email.ts schema and any duplicate
declaration around the second occurrence so both places use true), ensuring the
opt-out behavior remains intact.

})
.strip()
.catch(() => ({ ...DEFAULT_DOCUMENT_EMAIL_SETTINGS }));
Expand Down Expand Up @@ -115,5 +115,5 @@ export const DEFAULT_DOCUMENT_EMAIL_SETTINGS: TDocumentEmailSettings = {
ownerDocumentCompleted: true,
ownerRecipientExpired: true,
ownerDocumentCreated: true,
ownerReminderDigest: true,
ownerReminderDigest: false,
};
22 changes: 21 additions & 1 deletion packages/lib/types/document-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const ZDocumentMetaUploadSignatureEnabledSchema = z
* all corresponding areas where this is used (some places that use this needs to pass
* it through to another function).
*/
export const ZDocumentMetaCreateSchema = z.object({
export const ZDocumentMetaFieldsSchema = z.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
Expand All @@ -131,6 +131,26 @@ export const ZDocumentMetaCreateSchema = z.object({
emailReplyTo: zEmail().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.nullish(),
envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(),
reminderEnabled: z.boolean().optional(),
reminderIntervalDays: z.number().int().min(1).max(30).optional(),
});

export const ZDocumentMetaCreateSchema = ZDocumentMetaFieldsSchema.superRefine((data, ctx) => {
if (data.reminderEnabled && !data.reminderIntervalDays) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: msg`Reminder interval is required when reminders are enabled`.id,
path: ['reminderIntervalDays'],
});
}

if (data.reminderEnabled && !data.envelopeExpirationPeriod) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: msg`An expiration period is required to enable reminders`.id,
path: ['reminderEnabled'],
});
}
});

export type TDocumentMetaCreate = z.infer<typeof ZDocumentMetaCreateSchema>;
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/types/envelope-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ export const ZEditorEnvelopeSchema = EnvelopeSchema.pick({
emailId: true,
emailReplyTo: true,
envelopeExpirationPeriod: true,
reminderEnabled: true,
reminderIntervalDays: true,
}),
recipients: ZEnvelopeRecipientLiteSchema.array(),
fields: ZEnvelopeFieldSchema.array(),
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/types/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
emailId: true,
emailReplyTo: true,
envelopeExpirationPeriod: true,
reminderEnabled: true,
reminderIntervalDays: true,
}),
recipients: ZEnvelopeRecipientLiteSchema.array(),
fields: ZEnvelopeFieldSchema.array(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';

import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
import { ZDocumentMetaFieldsSchema } from '@documenso/lib/types/document-meta';

import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
Expand All @@ -18,7 +18,7 @@ export const distributeEnvelopeMeta: TrpcRouteMeta = {

export const ZDistributeEnvelopeRequestSchema = z.object({
envelopeId: z.string().describe('The ID of the envelope to send.'),
meta: ZDocumentMetaUpdateSchema.pick({
meta: ZDocumentMetaFieldsSchema.pick({
subject: true,
message: true,
timezone: true,
Expand Down
39 changes: 39 additions & 0 deletions packages/ui/components/document/document-email-checkboxes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,45 @@ export const DocumentEmailCheckboxes = ({
</Tooltip>
</label>
</div>

<div className="flex flex-row items-center">
<Checkbox
id={DocumentEmailEvents.OwnerReminderDigest}
className="h-5 w-5"
checked={value.ownerReminderDigest}
onCheckedChange={(checked) =>
onChange({ ...value, [DocumentEmailEvents.OwnerReminderDigest]: Boolean(checked) })
}
/>

<label
className="ml-2 flex flex-row items-center text-sm text-muted-foreground"
htmlFor={DocumentEmailEvents.OwnerReminderDigest}
>
<Trans>Send me a reminder digest when recipients haven't signed</Trans>

<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>

<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
<h2>
<strong>
<Trans>Reminder digest email</Trans>
</strong>
</h2>

<p>
<Trans>
When reminders are sent to unsigned recipients, this aggregates all pending
documents for your team into a single digest email sent to you.
</Trans>
</p>
</TooltipContent>
</Tooltip>
</label>
</div>
</div>
);
};
Loading