import { useEffect, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { Recipient } from '@prisma/client'; import { Hash, Loader } from 'lucide-react'; import { useRevalidator } from 'react-router'; import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number'; 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 { ZNumberFieldMeta } from '@documenso/lib/types/field-meta'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; 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 { Input } from '@documenso/ui/primitives/input'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { DocumentSigningFieldContainer } from './document-signing-field-container'; type ValidationErrors = { isNumber: string[]; required: string[]; minValue: string[]; maxValue: string[]; numberFormat: string[]; }; export type DocumentSigningNumberFieldProps = { field: FieldWithSignature; recipient: Recipient; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void; }; export const DocumentSigningNumberField = ({ field, recipient, onSignField, onUnsignField, }: DocumentSigningNumberFieldProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { revalidate } = useRevalidator(); const [showRadioModal, setShowRadioModal] = useState(false); const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null; const isReadOnly = parsedFieldMeta?.readOnly; const defaultValue = parsedFieldMeta?.value; const [localNumber, setLocalNumber] = useState( parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0', ); const initialErrors: ValidationErrors = { isNumber: [], required: [], minValue: [], maxValue: [], numberFormat: [], }; const [errors, setErrors] = useState(initialErrors); 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 isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; const handleNumberChange = (e: React.ChangeEvent) => { const text = e.target.value; setLocalNumber(text); if (parsedFieldMeta) { const validationErrors = validateNumberField(text, parsedFieldMeta, true); setErrors({ isNumber: validationErrors.filter((error) => error.includes('valid number')), required: validationErrors.filter((error) => error.includes('required')), minValue: validationErrors.filter((error) => error.includes('minimum value')), maxValue: validationErrors.filter((error) => error.includes('maximum value')), numberFormat: validationErrors.filter((error) => error.includes('number format')), }); } else { const validationErrors = validateNumberField(text); setErrors((prevErrors) => ({ ...prevErrors, isNumber: validationErrors.filter((error) => error.includes('valid number')), })); } }; const onDialogSignClick = () => { setShowRadioModal(false); void executeActionAuthProcedure({ onReauthFormSubmit: async (authOptions) => await onSign(authOptions), actionTarget: field.type, }); }; const onSign = async (authOptions?: TRecipientActionAuth) => { try { if (!localNumber || Object.values(errors).some((error) => error.length > 0)) { return; } const payload: TSignFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, value: localNumber, isBase64: true, authOptions, }; if (onSignField) { await onSignField(payload); return; } await signFieldWithToken(payload); setLocalNumber(''); await revalidate(); } catch (err) { const error = AppError.parseError(err); if (error.code === AppErrorCode.UNAUTHORIZED) { throw error; } console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while signing the document.`), variant: 'destructive', }); } }; const onPreSign = () => { setShowRadioModal(true); if (localNumber && parsedFieldMeta) { const validationErrors = validateNumberField(localNumber, parsedFieldMeta, true); setErrors({ isNumber: validationErrors.filter((error) => error.includes('valid number')), required: validationErrors.filter((error) => error.includes('required')), minValue: validationErrors.filter((error) => error.includes('minimum value')), maxValue: validationErrors.filter((error) => error.includes('maximum value')), numberFormat: validationErrors.filter((error) => error.includes('number format')), }); } return false; }; const onRemove = async () => { try { const payload: TRemovedSignedFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, }; if (onUnsignField) { await onUnsignField(payload); return; } await removeSignedFieldWithToken(payload); setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : ''); await revalidate(); } catch (err) { console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while removing the signature.`), variant: 'destructive', }); } }; useEffect(() => { if (!showRadioModal) { setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0'); setErrors(initialErrors); } }, [showRadioModal]); useEffect(() => { if ( (!field.inserted && defaultValue && localNumber) || (!field.inserted && isReadOnly && defaultValue) ) { void executeActionAuthProcedure({ onReauthFormSubmit: async (authOptions) => await onSign(authOptions), actionTarget: field.type, }); } }, []); let fieldDisplayName = 'Number'; if (parsedFieldMeta?.label) { fieldDisplayName = parsedFieldMeta.label.length > 10 ? parsedFieldMeta.label.substring(0, 10) + '...' : parsedFieldMeta.label; } const userInputHasErrors = Object.values(errors).some((error) => error.length > 0); return ( {isLoading && (
)} {!field.inserted && (

{' '} {fieldDisplayName}

)} {field.inserted && (

{field.customText}

)} {parsedFieldMeta?.label ? parsedFieldMeta?.label : Number}
{userInputHasErrors && (
{errors.isNumber?.map((error, index) => (

{error}

))} {errors.required?.map((error, index) => (

{error}

))} {errors.minValue?.map((error, index) => (

{error}

))} {errors.maxValue?.map((error, index) => (

{error}

))} {errors.numberFormat?.map((error, index) => (

{error}

))}
)}
); };