feat: sign document with a custom text

This commit is contained in:
Ephraim Atta-Duncan
2024-02-17 08:26:30 +00:00
parent 2815b1a809
commit 5687503dfc
4 changed files with 211 additions and 0 deletions

View File

@ -29,6 +29,7 @@ import { NameField } from './name-field';
import { NoLongerAvailable } from './no-longer-available'; import { NoLongerAvailable } from './no-longer-available';
import { SigningProvider } from './provider'; import { SigningProvider } from './provider';
import { SignatureField } from './signature-field'; import { SignatureField } from './signature-field';
import { TextField } from './text-field';
export type SigningPageProps = { export type SigningPageProps = {
params: { params: {
@ -168,6 +169,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
.with(FieldType.EMAIL, () => ( .with(FieldType.EMAIL, () => (
<EmailField key={field.id} field={field} recipient={recipient} /> <EmailField key={field.id} field={field} recipient={recipient} />
)) ))
.with(FieldType.TEXT, () => (
<TextField key={field.id} field={field} recipient={recipient} />
))
.otherwise(() => null), .otherwise(() => null),
)} )}
</ElementVisible> </ElementVisible>

View File

@ -9,6 +9,8 @@ export type SigningContextValue = {
setEmail: (_value: string) => void; setEmail: (_value: string) => void;
signature: string | null; signature: string | null;
setSignature: (_value: string | null) => void; setSignature: (_value: string | null) => void;
customText: string;
setCustomText: (_value: string) => void;
}; };
const SigningContext = createContext<SigningContextValue | null>(null); const SigningContext = createContext<SigningContextValue | null>(null);
@ -31,6 +33,7 @@ export interface SigningProviderProps {
fullName?: string | null; fullName?: string | null;
email?: string | null; email?: string | null;
signature?: string | null; signature?: string | null;
customText?: string | null;
children: React.ReactNode; children: React.ReactNode;
} }
@ -38,11 +41,13 @@ export const SigningProvider = ({
fullName: initialFullName, fullName: initialFullName,
email: initialEmail, email: initialEmail,
signature: initialSignature, signature: initialSignature,
customText: initialCustomText,
children, children,
}: SigningProviderProps) => { }: SigningProviderProps) => {
const [fullName, setFullName] = useState(initialFullName || ''); const [fullName, setFullName] = useState(initialFullName || '');
const [email, setEmail] = useState(initialEmail || ''); const [email, setEmail] = useState(initialEmail || '');
const [signature, setSignature] = useState(initialSignature || null); const [signature, setSignature] = useState(initialSignature || null);
const [customText, setCustomText] = useState(initialCustomText || '');
return ( return (
<SigningContext.Provider <SigningContext.Provider
@ -53,6 +58,8 @@ export const SigningProvider = ({
setEmail, setEmail,
signature, signature,
setSignature, setSignature,
customText,
setCustomText,
}} }}
> >
{children} {children}

View File

@ -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 (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Signature">
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground text-lg duration-200">Text</p>
)}
{field.inserted && <p className="text-muted-foreground duration-200">{field.customText}</p>}
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
<DialogContent>
<DialogTitle>
Enter a Text <span className="text-muted-foreground">({recipient.email})</span>
</DialogTitle>
<div className="">
<Label htmlFor="signature">Custom Text</Label>
<Textarea
id="custom-text"
className="border-border mt-2 h-44 w-full rounded-md border"
onChange={(e) => setLocalCustomText(e.target.value)}
/>
</div>
<DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary"
onClick={() => {
setShowCustomTextModal(false);
setLocalCustomText('');
}}
>
Cancel
</Button>
<Button
type="button"
className="flex-1"
disabled={!localText}
onClick={() => {
setShowCustomTextModal(false);
setIsLocalSignatureSet(true);
void onSign('local');
}}
>
Save Text
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</SigningFieldContainer>
);
};

View File

@ -552,6 +552,28 @@ export const AddFieldsFormPartial = ({
</CardContent> </CardContent>
</Card> </Card>
</button> </button>
<button
type="button"
className="group h-full w-full"
onClick={() => setSelectedField(FieldType.TEXT)}
onMouseDown={() => setSelectedField(FieldType.TEXT)}
data-selected={selectedField === FieldType.TEXT ? true : undefined}
>
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
)}
>
{'Text'}
</p>
<p className="text-muted-foreground mt-2 text-xs">Custom Text</p>
</CardContent>
</Card>
</button>
</fieldset> </fieldset>
</div> </div>
</div> </div>