diff --git a/.env.example b/.env.example
index 87ad09a63..7b8872b69 100644
--- a/.env.example
+++ b/.env.example
@@ -136,3 +136,5 @@ E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
NEXT_PRIVATE_LOGGER_FILE_PATH=
+# [[PLAIN SUPPORT]]
+NEXT_PRIVATE_PLAIN_API_KEY=
diff --git a/.gitignore b/.gitignore
index f31f951a7..9e622a76f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,4 +56,7 @@ logs.json
# claude
.claude
-CLAUDE.md
\ No newline at end of file
+CLAUDE.md
+
+# agents
+.specs
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..b6fba867d
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,57 @@
+# Agent Guidelines for Documenso
+
+## Build/Test/Lint Commands
+
+- `npm run build` - Build all packages
+- `npm run lint` - Lint all packages
+- `npm run lint:fix` - Auto-fix linting issues
+- `npm run test:e2e` - Run E2E tests with Playwright
+- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
+- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
+- `npm run format` - Format code with Prettier
+- `npm run dev` - Start development server for Remix app
+
+## Code Style Guidelines
+
+- Use TypeScript for all code; prefer `type` over `interface`
+- Use functional components with `const Component = () => {}`
+- Never use classes; prefer functional/declarative patterns
+- Use descriptive variable names with auxiliary verbs (isLoading, hasError)
+- Directory names: lowercase with dashes (auth-wizard)
+- Use named exports for components
+- Never use 'use client' directive
+- Never use 1-line if statements
+- Structure files: exported component, subcomponents, helpers, static content, types
+
+## Error Handling & Validation
+
+- Use custom AppError class when throwing errors
+- When catching errors on the frontend use `const error = AppError.parse(error)` to get the error code
+- Use early returns and guard clauses
+- Use Zod for form validation and react-hook-form for forms
+- Use error boundaries for unexpected errors
+
+## UI & Styling
+
+- Use Shadcn UI, Radix, and Tailwind CSS with mobile-first approach
+- Use `
+ ) : (
+ <>
+
+
+
+ Some signers have not been assigned a signature field. Please assign at least 1
+ signature field to each signer before proceeding.
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx b/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx
new file mode 100644
index 000000000..92d55b71b
--- /dev/null
+++ b/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx
@@ -0,0 +1,113 @@
+import { useState } from 'react';
+
+import { useLingui } from '@lingui/react/macro';
+import { Trans } from '@lingui/react/macro';
+import { EnvelopeType } from '@prisma/client';
+import { useNavigate } from 'react-router';
+
+import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
+import { trpc } from '@documenso/trpc/react';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { useCurrentTeam } from '~/providers/team';
+
+type EnvelopeDuplicateDialogProps = {
+ envelopeId: string;
+ envelopeType: EnvelopeType;
+ trigger?: React.ReactNode;
+};
+
+export const EnvelopeDuplicateDialog = ({
+ envelopeId,
+ envelopeType,
+ trigger,
+}: EnvelopeDuplicateDialogProps) => {
+ const navigate = useNavigate();
+
+ const [open, setOpen] = useState(false);
+
+ const { toast } = useToast();
+ const { t } = useLingui();
+
+ const team = useCurrentTeam();
+
+ const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } =
+ trpc.envelope.duplicate.useMutation({
+ onSuccess: async ({ duplicatedEnvelopeId }) => {
+ toast({
+ title: t`Envelope Duplicated`,
+ description: t`Your envelope has been successfully duplicated.`,
+ duration: 5000,
+ });
+
+ const path =
+ envelopeType === EnvelopeType.DOCUMENT
+ ? formatDocumentsPath(team.url)
+ : formatTemplatesPath(team.url);
+
+ await navigate(`${path}/${duplicatedEnvelopeId}/edit`);
+ setOpen(false);
+ },
+ });
+
+ const onDuplicate = async () => {
+ try {
+ await duplicateEnvelope({ envelopeId });
+ } catch {
+ toast({
+ title: t`Something went wrong`,
+ description: t`This document could not be duplicated at this time. Please try again.`,
+ variant: 'destructive',
+ duration: 7500,
+ });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx b/apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
new file mode 100644
index 000000000..dfee36496
--- /dev/null
+++ b/apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
@@ -0,0 +1,134 @@
+import { useState } from 'react';
+
+import { useLingui } from '@lingui/react/macro';
+import { Trans } from '@lingui/react/macro';
+
+import { trpc } from '@documenso/trpc/react';
+import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+export type EnvelopeItemDeleteDialogProps = {
+ canItemBeDeleted: boolean;
+ envelopeId: string;
+ envelopeItemId: string;
+ envelopeItemTitle: string;
+ onDelete?: (envelopeItemId: string) => void;
+ trigger?: React.ReactNode;
+};
+
+export const EnvelopeItemDeleteDialog = ({
+ trigger,
+ canItemBeDeleted,
+ envelopeId,
+ envelopeItemId,
+ envelopeItemTitle,
+ onDelete,
+}: EnvelopeItemDeleteDialogProps) => {
+ const [open, setOpen] = useState(false);
+
+ const { t } = useLingui();
+ const { toast } = useToast();
+
+ const { mutateAsync: deleteEnvelopeItem, isPending: isDeleting } =
+ trpc.envelope.item.delete.useMutation({
+ onSuccess: () => {
+ toast({
+ title: t`Success`,
+ description: t`You have successfully removed this envelope item.`,
+ duration: 5000,
+ });
+
+ onDelete?.(envelopeItemId);
+
+ setOpen(false);
+ },
+ onError: () => {
+ toast({
+ title: t`An unknown error occurred`,
+ description: t`We encountered an unknown error while attempting to remove this envelope item. Please try again later.`,
+ variant: 'destructive',
+ duration: 10000,
+ });
+ },
+ });
+
+ return (
+
+ );
+};
diff --git a/apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx b/apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
new file mode 100644
index 000000000..6a60ba81b
--- /dev/null
+++ b/apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
@@ -0,0 +1,187 @@
+import { useEffect, useState } from 'react';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react/macro';
+import { Trans } from '@lingui/react/macro';
+import { DocumentStatus, EnvelopeType, type Recipient, SigningStatus } from '@prisma/client';
+import { useForm } from 'react-hook-form';
+import * as z from 'zod';
+
+import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
+import type { TEnvelope } from '@documenso/lib/types/envelope';
+import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
+import { trpc as trpcReact } from '@documenso/trpc/react';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import { Checkbox } from '@documenso/ui/primitives/checkbox';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+} from '@documenso/ui/primitives/form/form';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { StackAvatar } from '../general/stack-avatar';
+
+export type EnvelopeRedistributeDialogProps = {
+ envelope: Pick & {
+ recipients: Recipient[];
+ };
+ trigger?: React.ReactNode;
+};
+
+export const ZEnvelopeRedistributeFormSchema = z.object({
+ recipients: z.array(z.number()).min(1, {
+ message: msg`You must select at least one item`.id,
+ }),
+});
+
+export type TEnvelopeRedistributeFormSchema = z.infer;
+
+export const EnvelopeRedistributeDialog = ({
+ envelope,
+ trigger,
+}: EnvelopeRedistributeDialogProps) => {
+ const recipients = envelope.recipients;
+
+ const { toast } = useToast();
+ const { t } = useLingui();
+
+ const [isOpen, setIsOpen] = useState(false);
+
+ const { mutateAsync: redistributeEnvelope } = trpcReact.envelope.redistribute.useMutation();
+
+ const form = useForm({
+ defaultValues: {
+ recipients: [],
+ },
+ resolver: zodResolver(ZEnvelopeRedistributeFormSchema),
+ });
+
+ const {
+ handleSubmit,
+ formState: { isSubmitting },
+ } = form;
+
+ const onFormSubmit = async ({ recipients }: TEnvelopeRedistributeFormSchema) => {
+ try {
+ await redistributeEnvelope({ envelopeId: envelope.id, recipients });
+
+ toast({
+ title: t`Envelope resent`,
+ description: t`Your envelope has been resent successfully.`,
+ duration: 5000,
+ });
+
+ setIsOpen(false);
+ } catch (err) {
+ toast({
+ title: t`Something went wrong`,
+ description: t`This envelope could not be resent at this time. Please try again.`,
+ variant: 'destructive',
+ duration: 7500,
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (!isOpen) {
+ form.reset();
+ }
+ }, [isOpen]);
+
+ if (envelope.status !== DocumentStatus.PENDING || envelope.type !== EnvelopeType.DOCUMENT) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx
index 6a21895d3..8bfcbe3e5 100644
--- a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx
+++ b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx
@@ -65,9 +65,9 @@ export const PasskeyCreateDialog = ({ trigger, onSuccess, ...props }: PasskeyCre
});
const { mutateAsync: createPasskeyRegistrationOptions, isPending } =
- trpc.auth.createPasskeyRegistrationOptions.useMutation();
+ trpc.auth.passkey.createRegistrationOptions.useMutation();
- const { mutateAsync: createPasskey } = trpc.auth.createPasskey.useMutation();
+ const { mutateAsync: createPasskey } = trpc.auth.passkey.create.useMutation();
const onFormSubmit = async ({ passkeyName }: TCreatePasskeyFormSchema) => {
setFormError(null);
diff --git a/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx b/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
index bcd624290..4ef2de031 100644
--- a/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
+++ b/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
@@ -4,14 +4,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
-import type { Template, TemplateDirectLink } from '@prisma/client';
-import { TemplateType } from '@prisma/client';
+import { type TemplateDirectLink, TemplateType } from '@prisma/client';
import type * as DialogPrimitive from '@radix-ui/react-dialog';
import { CheckCircle2Icon, CircleIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { P, match } from 'ts-pattern';
import { z } from 'zod';
+import { type Template } from '@documenso/prisma/types/template-legacy-schema';
import { trpc } from '@documenso/trpc/react';
import {
MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH,
@@ -52,7 +52,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCurrentTeam } from '~/providers/team';
export type ManagePublicTemplateDialogProps = {
- directTemplates: (Template & {
+ directTemplates: (Omit & {
directLink: Pick;
})[];
initialTemplateId?: number | null;
diff --git a/apps/remix/app/components/dialogs/sign-field-dropdown-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-dropdown-dialog.tsx
new file mode 100644
index 000000000..cf80c6009
--- /dev/null
+++ b/apps/remix/app/components/dialogs/sign-field-dropdown-dialog.tsx
@@ -0,0 +1,117 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { createCallable } from 'react-call';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import type { TDropdownFieldMeta } from '@documenso/lib/types/field-meta';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@documenso/ui/primitives/select';
+
+const ZSignFieldDropdownFormSchema = z.object({
+ dropdown: z.string().min(1, { message: msg`Option is required`.id }),
+});
+
+type TSignFieldDropdownFormSchema = z.infer;
+
+export type SignFieldDropdownDialogProps = {
+ fieldMeta: TDropdownFieldMeta;
+};
+
+export const SignFieldDropdownDialog = createCallable(
+ ({ call, fieldMeta }) => {
+ const { t } = useLingui();
+
+ const values = fieldMeta.values?.map((value) => value.value) ?? [];
+
+ const form = useForm({
+ resolver: zodResolver(ZSignFieldDropdownFormSchema),
+ defaultValues: {
+ dropdown: fieldMeta.defaultValue,
+ },
+ });
+
+ return (
+
+ );
+ },
+);
diff --git a/apps/remix/app/components/dialogs/sign-field-email-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
new file mode 100644
index 000000000..1ced42000
--- /dev/null
+++ b/apps/remix/app/components/dialogs/sign-field-email-dialog.tsx
@@ -0,0 +1,91 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { Trans } from '@lingui/react/macro';
+import { createCallable } from 'react-call';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+
+const ZSignFieldEmailFormSchema = z.object({
+ email: z.string().min(1, { message: msg`Email is required`.id }),
+});
+
+type TSignFieldEmailFormSchema = z.infer;
+
+export type SignFieldEmailDialogProps = Record;
+
+export const SignFieldEmailDialog = createCallable(
+ ({ call }) => {
+ const form = useForm({
+ resolver: zodResolver(ZSignFieldEmailFormSchema),
+ defaultValues: {
+ email: '',
+ },
+ });
+
+ return (
+
+ );
+ },
+);
diff --git a/apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
new file mode 100644
index 000000000..e171da56f
--- /dev/null
+++ b/apps/remix/app/components/dialogs/sign-field-initials-dialog.tsx
@@ -0,0 +1,97 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { Trans } from '@lingui/react/macro';
+import { createCallable } from 'react-call';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+
+const ZSignFieldInitialsFormSchema = z.object({
+ initials: z.string().min(1, { message: msg`Initials are required`.id }),
+});
+
+type TSignFieldInitialsFormSchema = z.infer;
+
+export type SignFieldInitialsDialogProps = {
+ //
+};
+
+export const SignFieldInitialsDialog = createCallable(
+ ({ call }) => {
+ const form = useForm({
+ resolver: zodResolver(ZSignFieldInitialsFormSchema),
+ defaultValues: {
+ initials: '',
+ },
+ });
+
+ return (
+
+ );
+ },
+);
diff --git a/apps/remix/app/components/dialogs/sign-field-name-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
new file mode 100644
index 000000000..eebfd7e4a
--- /dev/null
+++ b/apps/remix/app/components/dialogs/sign-field-name-dialog.tsx
@@ -0,0 +1,93 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { Trans } from '@lingui/react/macro';
+import { createCallable } from 'react-call';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+
+const ZSignFieldNameFormSchema = z.object({
+ name: z.string().min(1, { message: msg`Name is required`.id }),
+});
+
+type TSignFieldNameFormSchema = z.infer;
+
+export type SignFieldNameDialogProps = {
+ //
+};
+
+export const SignFieldNameDialog = createCallable(
+ ({ call }) => {
+ const form = useForm({
+ resolver: zodResolver(ZSignFieldNameFormSchema),
+ defaultValues: {
+ name: '',
+ },
+ });
+
+ return (
+
+ );
+ },
+);
diff --git a/apps/remix/app/components/dialogs/sign-field-number-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
new file mode 100644
index 000000000..4476f7965
--- /dev/null
+++ b/apps/remix/app/components/dialogs/sign-field-number-dialog.tsx
@@ -0,0 +1,144 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react/macro';
+import { Trans } from '@lingui/react/macro';
+import { createCallable } from 'react-call';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import type { TNumberFieldMeta } from '@documenso/lib/types/field-meta';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import { numberFormatValues } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Input } from '@documenso/ui/primitives/input';
+
+const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
+ let schema = z.coerce.number({
+ invalid_type_error: msg`Please enter a valid number`.id, // Todo: Envelopes - Check that this works
+ });
+
+ const { numberFormat, minValue, maxValue } = fieldMeta;
+
+ if (typeof minValue === 'number') {
+ schema = schema.min(minValue);
+ }
+
+ if (typeof maxValue === 'number') {
+ schema = schema.max(maxValue);
+ }
+
+ if (numberFormat) {
+ const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
+
+ if (!foundRegex) {
+ return schema;
+ }
+
+ return schema.refine(
+ (value) => {
+ return foundRegex.test(value.toString());
+ },
+ {
+ message: `Number needs to be formatted as ${numberFormat}`,
+ // Todo: Envelopes
+ // message: msg`Number needs to be formatted as ${numberFormat}`.id,
+ },
+ );
+ }
+
+ return schema;
+};
+
+export type SignFieldNumberDialogProps = {
+ fieldMeta: TNumberFieldMeta;
+};
+
+export const SignFieldNumberDialog = createCallable(
+ ({ call, fieldMeta }) => {
+ const { t } = useLingui();
+
+ const ZSignFieldNumberFormSchema = z.object({
+ number: createNumberFieldSchema(fieldMeta),
+ });
+
+ const form = useForm>({
+ resolver: zodResolver(ZSignFieldNumberFormSchema),
+ defaultValues: {
+ number: undefined,
+ },
+ });
+
+ return (
+
+ );
+ },
+);
diff --git a/apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
new file mode 100644
index 000000000..6ce8c8e77
--- /dev/null
+++ b/apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx
@@ -0,0 +1,76 @@
+import { useState } from 'react';
+
+import { Trans } from '@lingui/react/macro';
+import { createCallable } from 'react-call';
+
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
+
+import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure';
+
+export type SignFieldSignatureDialogProps = {
+ initialSignature?: string;
+ typedSignatureEnabled?: boolean;
+ uploadSignatureEnabled?: boolean;
+ drawSignatureEnabled?: boolean;
+};
+
+export const SignFieldSignatureDialog = createCallable<
+ SignFieldSignatureDialogProps,
+ string | null
+>(
+ ({
+ call,
+ typedSignatureEnabled,
+ uploadSignatureEnabled,
+ drawSignatureEnabled,
+ initialSignature,
+ }) => {
+ const [localSignature, setLocalSignature] = useState(initialSignature);
+
+ return (
+
+ );
+ },
+);
diff --git a/apps/remix/app/components/dialogs/sign-field-text-dialog.tsx b/apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
new file mode 100644
index 000000000..1e3b37b09
--- /dev/null
+++ b/apps/remix/app/components/dialogs/sign-field-text-dialog.tsx
@@ -0,0 +1,120 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { msg } from '@lingui/core/macro';
+import { Plural, useLingui } from '@lingui/react/macro';
+import { Trans } from '@lingui/react/macro';
+import { createCallable } from 'react-call';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+
+import type { TTextFieldMeta } from '@documenso/lib/types/field-meta';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import { Textarea } from '@documenso/ui/primitives/textarea';
+
+const ZSignFieldTextFormSchema = z.object({
+ text: z.string().min(1, { message: msg`Text is required`.id }),
+});
+
+type TSignFieldTextFormSchema = z.infer;
+
+export type SignFieldTextDialogProps = {
+ fieldMeta?: TTextFieldMeta;
+};
+
+export const SignFieldTextDialog = createCallable(
+ ({ call, fieldMeta }) => {
+ const { t } = useLingui();
+
+ const form = useForm({
+ resolver: zodResolver(ZSignFieldTextFormSchema),
+ defaultValues: {
+ text: '',
+ },
+ });
+
+ return (
+
+ );
+ },
+);
diff --git a/apps/remix/app/components/dialogs/template-create-dialog.tsx b/apps/remix/app/components/dialogs/template-create-dialog.tsx
index 1e05dadba..76d4e0c8a 100644
--- a/apps/remix/app/components/dialogs/template-create-dialog.tsx
+++ b/apps/remix/app/components/dialogs/template-create-dialog.tsx
@@ -44,7 +44,9 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) =>
const [showTemplateCreateDialog, setShowTemplateCreateDialog] = useState(false);
const [isUploadingFile, setIsUploadingFile] = useState(false);
- const onFileDrop = async (file: File) => {
+ const onFileDrop = async (files: File[]) => {
+ const file = files[0];
+
if (isUploadingFile) {
return;
}
@@ -54,7 +56,7 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) =>
try {
const response = await putPdfFile(file);
- const { id } = await createTemplate({
+ const { legacyTemplateId: id } = await createTemplate({
title: file.name,
templateDocumentDataId: response.id,
folderId: folderId,
diff --git a/apps/remix/app/components/dialogs/template-direct-link-dialog-wrapper.tsx b/apps/remix/app/components/dialogs/template-direct-link-dialog-wrapper.tsx
deleted file mode 100644
index 60ff06715..000000000
--- a/apps/remix/app/components/dialogs/template-direct-link-dialog-wrapper.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useState } from 'react';
-
-import { Trans } from '@lingui/react/macro';
-import type { Recipient, Template, TemplateDirectLink } from '@prisma/client';
-import { LinkIcon } from 'lucide-react';
-
-import { Button } from '@documenso/ui/primitives/button';
-
-import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog';
-
-export type TemplateDirectLinkDialogWrapperProps = {
- template: Template & { directLink?: TemplateDirectLink | null; recipients: Recipient[] };
-};
-
-export const TemplateDirectLinkDialogWrapper = ({
- template,
-}: TemplateDirectLinkDialogWrapperProps) => {
- const [isTemplateDirectLinkOpen, setTemplateDirectLinkOpen] = useState(false);
-
- return (
-