diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 99b9d1dd7..83cdb93e2 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -29,6 +29,7 @@ import { NameField } from './name-field'; import { NoLongerAvailable } from './no-longer-available'; import { SigningProvider } from './provider'; import { SignatureField } from './signature-field'; +import { TextField } from './text-field'; export type SigningPageProps = { params: { @@ -168,6 +169,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp .with(FieldType.EMAIL, () => ( )) + .with(FieldType.TEXT, () => ( + + )) .otherwise(() => null), )} diff --git a/apps/web/src/app/(signing)/sign/[token]/provider.tsx b/apps/web/src/app/(signing)/sign/[token]/provider.tsx index 454007cb0..6531e8a40 100644 --- a/apps/web/src/app/(signing)/sign/[token]/provider.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/provider.tsx @@ -9,6 +9,8 @@ export type SigningContextValue = { setEmail: (_value: string) => void; signature: string | null; setSignature: (_value: string | null) => void; + customText: string; + setCustomText: (_value: string) => void; }; const SigningContext = createContext(null); @@ -31,6 +33,7 @@ export interface SigningProviderProps { fullName?: string | null; email?: string | null; signature?: string | null; + customText?: string | null; children: React.ReactNode; } @@ -38,11 +41,13 @@ export const SigningProvider = ({ fullName: initialFullName, email: initialEmail, signature: initialSignature, + customText: initialCustomText, children, }: SigningProviderProps) => { const [fullName, setFullName] = useState(initialFullName || ''); const [email, setEmail] = useState(initialEmail || ''); const [signature, setSignature] = useState(initialSignature || null); + const [customText, setCustomText] = useState(initialCustomText || ''); return ( {children} diff --git a/apps/web/src/app/(signing)/sign/[token]/text-field.tsx b/apps/web/src/app/(signing)/sign/[token]/text-field.tsx new file mode 100644 index 000000000..d324a50ba --- /dev/null +++ b/apps/web/src/app/(signing)/sign/[token]/text-field.tsx @@ -0,0 +1,178 @@ +'use client'; + +import { useEffect, useState, useTransition } from 'react'; + +import { useRouter } from 'next/navigation'; + +import { Loader } from 'lucide-react'; + +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; +import { Label } from '@documenso/ui/primitives/label'; +import { Textarea } from '@documenso/ui/primitives/textarea'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useRequiredSigningContext } from './provider'; +import { SigningFieldContainer } from './signing-field-container'; + +export type TextFieldProps = { + field: FieldWithSignature; + recipient: Recipient; +}; + +export const TextField = ({ field, recipient }: TextFieldProps) => { + const router = useRouter(); + + const { toast } = useToast(); + const { customText: providedCustomText, setCustomText: setProvidedCustomText } = + useRequiredSigningContext(); + + const [isPending, startTransition] = useTransition(); + + const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } = + trpc.field.signFieldWithToken.useMutation(); + + const { + mutateAsync: removeSignedFieldWithToken, + isLoading: isRemoveSignedFieldWithTokenLoading, + } = trpc.field.removeSignedFieldWithToken.useMutation(); + + const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; + + const [showCustomTextModal, setShowCustomTextModal] = useState(false); + const [localText, setLocalCustomText] = useState(''); + const [isLocalSignatureSet, setIsLocalSignatureSet] = useState(false); + + useEffect(() => { + if (!showCustomTextModal && !isLocalSignatureSet) { + setLocalCustomText(''); + } + }, [showCustomTextModal, isLocalSignatureSet]); + + const onSign = async (source: 'local' | 'provider' = 'provider') => { + try { + if (!providedCustomText && !localText) { + setIsLocalSignatureSet(false); + setShowCustomTextModal(true); + return; + } + + const value = source === 'local' && localText ? localText : providedCustomText ?? ''; + + if (!value) { + return; + } + + await signFieldWithToken({ + token: recipient.token, + fieldId: field.id, + value, + isBase64: true, + }); + + if (source === 'local' && !providedCustomText) { + setProvidedCustomText(localText); + } + + setLocalCustomText(''); + + startTransition(() => router.refresh()); + } catch (err) { + console.error(err); + + toast({ + title: 'Error', + description: 'An error occurred while signing the document.', + variant: 'destructive', + }); + } + }; + + const onRemove = async () => { + try { + // Necessary to reset the custom text if the user removes the signature + setProvidedCustomText(''); + + await removeSignedFieldWithToken({ + token: recipient.token, + fieldId: field.id, + }); + + startTransition(() => router.refresh()); + } catch (err) { + console.error(err); + + toast({ + title: 'Error', + description: 'An error occurred while removing the signature.', + variant: 'destructive', + }); + } + }; + + return ( + + {isLoading && ( + + + + )} + + {!field.inserted && ( + Text + )} + + {field.inserted && {field.customText}} + + + + + Enter a Text ({recipient.email}) + + + + Custom Text + + setLocalCustomText(e.target.value)} + /> + + + + + { + setShowCustomTextModal(false); + setLocalCustomText(''); + }} + > + Cancel + + + { + setShowCustomTextModal(false); + setIsLocalSignatureSet(true); + void onSign('local'); + }} + > + Save Text + + + + + + + ); +}; diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index a6266d1aa..f5b839158 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -552,6 +552,28 @@ export const AddFieldsFormPartial = ({ + + setSelectedField(FieldType.TEXT)} + onMouseDown={() => setSelectedField(FieldType.TEXT)} + data-selected={selectedField === FieldType.TEXT ? true : undefined} + > + + + + {'Text'} + + + Custom Text + + +
Text
{field.customText}
+ {'Text'} +
Custom Text