import { useEffect, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Plural, Trans } from '@lingui/react/macro'; import { Loader, Type } from 'lucide-react'; import { useRevalidator } from 'react-router'; import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { ZTextFieldMeta } from '@documenso/lib/types/field-meta'; import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta'; import { trpc } from '@documenso/trpc/react'; import type { TRemovedSignedFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema, } from '@documenso/trpc/server/field-router/schema'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; import { Textarea } from '@documenso/ui/primitives/textarea'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; export type DocumentSigningTextFieldProps = { field: FieldWithSignatureAndFieldMeta; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void; }; type ValidationErrors = { required: string[]; characterLimit: string[]; }; export type TextFieldProps = { field: FieldWithSignatureAndFieldMeta; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void; }; export const DocumentSigningTextField = ({ field, onSignField, onUnsignField, }: DocumentSigningTextFieldProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { revalidate } = useRevalidator(); const { recipient, isAssistantMode } = useDocumentSigningRecipientContext(); const initialErrors: ValidationErrors = { required: [], characterLimit: [], }; const [errors, setErrors] = useState(initialErrors); const userInputHasErrors = Object.values(errors).some((error) => error.length > 0); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext(); const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } = trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); const { mutateAsync: removeSignedFieldWithToken, isPending: isRemoveSignedFieldWithTokenLoading, } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); const safeFieldMeta = ZTextFieldMeta.safeParse(field.fieldMeta); const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; const shouldAutoSignField = (!field.inserted && parsedFieldMeta?.text) || (!field.inserted && parsedFieldMeta?.text && parsedFieldMeta?.readOnly); const [showCustomTextModal, setShowCustomTextModal] = useState(false); const [localText, setLocalCustomText] = useState(parsedFieldMeta?.text ?? ''); useEffect(() => { if (!showCustomTextModal) { setLocalCustomText(parsedFieldMeta?.text ?? ''); setErrors(initialErrors); } }, [showCustomTextModal]); const handleTextChange = (e: React.ChangeEvent) => { const text = e.target.value; setLocalCustomText(text); if (parsedFieldMeta) { const validationErrors = validateTextField(text, parsedFieldMeta, true); setErrors({ required: validationErrors.filter((error) => error.includes('required')), characterLimit: validationErrors.filter((error) => error.includes('character limit')), }); } }; /** * When the user clicks the sign button in the dialog where they enter the text field. */ const onDialogSignClick = () => { if (parsedFieldMeta) { const validationErrors = validateTextField(localText, parsedFieldMeta, true); if (validationErrors.length > 0) { setErrors({ required: validationErrors.filter((error) => error.includes('required')), characterLimit: validationErrors.filter((error) => error.includes('character limit')), }); return; } } setShowCustomTextModal(false); void executeActionAuthProcedure({ onReauthFormSubmit: async (authOptions) => await onSign(authOptions), actionTarget: field.type, }); }; const onPreSign = () => { setShowCustomTextModal(true); if (localText && parsedFieldMeta) { const validationErrors = validateTextField(localText, parsedFieldMeta, true); setErrors({ required: validationErrors.filter((error) => error.includes('required')), characterLimit: validationErrors.filter((error) => error.includes('character limit')), }); } return false; }; const onSign = async (authOptions?: TRecipientActionAuth) => { try { if (!localText || userInputHasErrors) { return; } const payload: TSignFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, value: localText, isBase64: true, authOptions, }; if (onSignField) { await onSignField(payload); return; } await signFieldWithToken(payload); setLocalCustomText(''); await revalidate(); } catch (err) { const error = AppError.parseError(err); if (error.code === AppErrorCode.UNAUTHORIZED) { throw error; } console.error(err); toast({ title: _(msg`Error`), description: isAssistantMode ? _(msg`An error occurred while signing as assistant.`) : _(msg`An error occurred while signing the document.`), variant: 'destructive', }); } }; const onRemove = async () => { try { const payload: TRemovedSignedFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, }; if (onUnsignField) { await onUnsignField(payload); return; } await removeSignedFieldWithToken(payload); setLocalCustomText(parsedFieldMeta?.text ?? ''); await revalidate(); } catch (err) { console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while removing the field.`), variant: 'destructive', }); } }; useEffect(() => { if (shouldAutoSignField) { void executeActionAuthProcedure({ onReauthFormSubmit: async (authOptions) => await onSign(authOptions), actionTarget: field.type, }); } }, []); const parsedField = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : undefined; const labelDisplay = parsedField?.label && parsedField.label.length < 20 ? parsedField.label : parsedField?.label ? parsedField?.label.substring(0, 20) + '...' : undefined; const textDisplay = parsedField?.text && parsedField.text.length < 20 ? parsedField.text : parsedField?.text ? parsedField?.text.substring(0, 20) + '...' : undefined; const fieldDisplayName = labelDisplay ? labelDisplay : textDisplay; const charactersRemaining = (parsedFieldMeta?.characterLimit ?? 0) - (localText.length ?? 0); return ( {isLoading && (
)} {!field.inserted && (

{fieldDisplayName || Text}

)} {field.inserted && (

{field.customText.length < 20 ? field.customText : field.customText.substring(0, 20) + '...'}

)} {parsedFieldMeta?.label ? parsedFieldMeta?.label : Text}