import { useEffect, useMemo, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import type { Recipient } from '@prisma/client'; import { Loader } from 'lucide-react'; import { useRevalidator } from 'react-router'; 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 { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta'; import { fromCheckboxValue, toCheckboxValue } from '@documenso/lib/universal/field-checkbox'; 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 { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { checkboxValidationSigns } from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants'; import { Label } from '@documenso/ui/primitives/label'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { DocumentSigningFieldContainer } from './document-signing-field-container'; export type DocumentSigningCheckboxFieldProps = { field: FieldWithSignatureAndFieldMeta; recipient: Recipient; onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void; onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void; }; export const DocumentSigningCheckboxField = ({ field, recipient, onSignField, onUnsignField, }: DocumentSigningCheckboxFieldProps) => { const { _ } = useLingui(); const { toast } = useToast(); const { revalidate } = useRevalidator(); const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext(); const parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta); const values = parsedFieldMeta.values?.map((item) => ({ ...item, value: item.value.length > 0 ? item.value : `empty-value-${item.id}`, })); const [checkedValues, setCheckedValues] = useState( values ?.map((item) => item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '', ) .filter(Boolean) || [], ); const isReadOnly = parsedFieldMeta.readOnly; const checkboxValidationRule = parsedFieldMeta.validationRule; const checkboxValidationLength = parsedFieldMeta.validationLength; const validationSign = checkboxValidationSigns.find( (sign) => sign.label === checkboxValidationRule, ); const isLengthConditionMet = useMemo(() => { if (!validationSign) return true; return ( (validationSign.value === '>=' && checkedValues.length >= (checkboxValidationLength || 0)) || (validationSign.value === '=' && checkedValues.length === (checkboxValidationLength || 0)) || (validationSign.value === '<=' && checkedValues.length <= (checkboxValidationLength || 0)) ); }, [checkedValues, validationSign, checkboxValidationLength]); 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 shouldAutoSignField = (!field.inserted && checkedValues.length > 0 && isLengthConditionMet) || (!field.inserted && isReadOnly && isLengthConditionMet); const onSign = async (authOptions?: TRecipientActionAuth) => { try { const payload: TSignFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, value: toCheckboxValue(checkedValues), isBase64: true, authOptions, }; if (onSignField) { await onSignField(payload); } else { await signFieldWithToken(payload); } 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 onRemove = async (fieldType?: string) => { try { const payload: TRemovedSignedFieldWithTokenMutationSchema = { token: recipient.token, fieldId: field.id, }; if (onUnsignField) { await onUnsignField(payload); } else { await removeSignedFieldWithToken(payload); } if (fieldType === 'Checkbox') { setCheckedValues([]); } await revalidate(); } catch (err) { console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while removing the signature.`), variant: 'destructive', }); } }; const handleCheckboxChange = (value: string, itemId: number) => { const updatedValue = value || `empty-value-${itemId}`; const updatedValues = checkedValues.includes(updatedValue) ? checkedValues.filter((v) => v !== updatedValue) : [...checkedValues, updatedValue]; setCheckedValues(updatedValues); }; const handleCheckboxOptionClick = async (item: { id: number; checked: boolean; value: string; }) => { let updatedValues: string[] = []; try { const isChecked = checkedValues.includes( item.value.length > 0 ? item.value : `empty-value-${item.id}`, ); if (!isChecked) { updatedValues = [ ...checkedValues, item.value.length > 0 ? item.value : `empty-value-${item.id}`, ]; await removeSignedFieldWithToken({ token: recipient.token, fieldId: field.id, }); if (isLengthConditionMet) { await signFieldWithToken({ token: recipient.token, fieldId: field.id, value: toCheckboxValue(checkedValues), isBase64: true, }); } } else { updatedValues = checkedValues.filter( (v) => v !== item.value && v !== `empty-value-${item.id}`, ); await removeSignedFieldWithToken({ token: recipient.token, fieldId: field.id, }); } } catch (err) { console.error(err); toast({ title: _(msg`Error`), description: _(msg`An error occurred while updating the signature.`), variant: 'destructive', }); } finally { setCheckedValues(updatedValues); await revalidate(); } }; useEffect(() => { if (shouldAutoSignField) { void executeActionAuthProcedure({ onReauthFormSubmit: async (authOptions) => await onSign(authOptions), actionTarget: field.type, }); } }, [checkedValues, isLengthConditionMet, field.inserted]); const parsedCheckedValues = useMemo( () => fromCheckboxValue(field.customText), [field.customText], ); return ( {isLoading && (
)} {!field.inserted && ( <> {!isLengthConditionMet && ( {validationSign?.label} {checkboxValidationLength} )}
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => { const itemValue = item.value || `empty-value-${item.id}`; return (
handleCheckboxChange(item.value, item.id)} />
); })}
)} {field.inserted && (
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => { const itemValue = item.value || `empty-value-${item.id}`; return (
void handleCheckboxOptionClick(item)} />
); })}
)}
); };