Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for uploading signature #905

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions apps/marketing/src/components/(marketing)/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,13 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
<SignaturePad
disabled={isSubmitting}
className="aspect-video w-full rounded-md border"
defaultValue={signatureDataUrl || ''}
onChange={setDraftSignatureDataUrl}
signature={{
value: signatureDataUrl,
type: 'DRAW',
}}
// Disabling the uploading of a signature for marketing website
uploadDisable={true}
onChange={(value: string | null, _: boolean) => setDraftSignatureDataUrl(value)}
/>

<DialogFooter>
Expand Down
13 changes: 9 additions & 4 deletions apps/web/src/app/(signing)/sign/[token]/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
const analytics = useAnalytics();
const { data: session } = useSession();

const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext();
const { fullName, signature, setFullName, setSignature, signatureType, setSignatureType } =
useRequiredSigningContext();

const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);

Expand Down Expand Up @@ -170,10 +171,14 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
signature={{
value: signature,
type: signatureType ?? 'DRAW',
}}
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
onChange={(v: string | null, isUploaded: boolean) => {
setSignature(v);
setSignatureType(isUploaded ? 'UPLOAD' : 'DRAW');
}}
/>
</CardContent>
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/(signing)/sign/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
email={recipient.email}
fullName={user?.email === recipient.email ? user.name : recipient.name}
signature={user?.email === recipient.email ? user.signature : undefined}
signatureType={user?.email === recipient.email ? user.signatureType : undefined}
>
<div className="mx-auto w-full max-w-screen-xl">
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/app/(signing)/sign/[token]/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

import { createContext, useContext, useState } from 'react';

import { SignatureType } from '@documenso/prisma/client';

export type SigningContextValue = {
fullName: string;
setFullName: (_value: string) => void;
email: string;
setEmail: (_value: string) => void;
signature: string | null;
setSignature: (_value: string | null) => void;
signatureType: SignatureType | null;
setSignatureType: (_value: SignatureType | null) => void;
};

const SigningContext = createContext<SigningContextValue | null>(null);
Expand All @@ -31,18 +35,23 @@ export interface SigningProviderProps {
fullName?: string | null;
email?: string | null;
signature?: string | null;
signatureType?: SignatureType | null;
children: React.ReactNode;
}

export const SigningProvider = ({
fullName: initialFullName,
email: initialEmail,
signature: initialSignature,
signatureType: initialSignatureType,
children,
}: SigningProviderProps) => {
const [fullName, setFullName] = useState(initialFullName || '');
const [email, setEmail] = useState(initialEmail || '');
const [signature, setSignature] = useState(initialSignature || null);
const [signatureType, setSignatureType] = useState<SignatureType | null>(
initialSignatureType || null,
);

return (
<SigningContext.Provider
Expand All @@ -53,6 +62,8 @@ export const SigningProvider = ({
setEmail,
signature,
setSignature,
signatureType,
setSignatureType,
}}
>
{children}
Expand Down
34 changes: 27 additions & 7 deletions apps/web/src/app/(signing)/sign/[token]/signature-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';

import type { Recipient } from '@documenso/prisma/client';
import { SignatureType } 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';
Expand All @@ -29,8 +30,11 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
const router = useRouter();

const { toast } = useToast();
const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredSigningContext();
const {
signature: providedSignature,
setSignature: setProvidedSignature,
setSignatureType: setProvidedSignatureType,
} = useRequiredSigningContext();

const [isPending, startTransition] = useTransition();

Expand All @@ -47,7 +51,10 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;

const [showSignatureModal, setShowSignatureModal] = useState(false);
const [localSignature, setLocalSignature] = useState<string | null>(null);
const [localSignature, setLocalSignature] = useState<{
value: string | null;
type: SignatureType | null;
} | null>();
Comment on lines +54 to +57

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isolating useState Type to respective type.ts file is recommended/

const [isLocalSignatureSet, setIsLocalSignatureSet] = useState(false);

const state = useMemo<SignatureFieldState>(() => {
Expand All @@ -70,13 +77,16 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {

const onSign = async (source: 'local' | 'provider' = 'provider') => {
try {
if (!providedSignature && !localSignature) {
if (!providedSignature && !localSignature?.value) {
setIsLocalSignatureSet(false);
setShowSignatureModal(true);
return;
}

const value = source === 'local' && localSignature ? localSignature : providedSignature ?? '';
const value =
source === 'local' && localSignature?.value
? localSignature.value
: providedSignature ?? '';

if (!value) {
return;
Expand All @@ -90,7 +100,8 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
});

if (source === 'local' && !providedSignature) {
setProvidedSignature(localSignature);
setProvidedSignature(localSignature?.value ?? '');
setProvidedSignatureType(localSignature?.type ?? 'DRAW');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use SignatureType.DRAW instead of 'DRAW'?

}

setLocalSignature(null);
Expand Down Expand Up @@ -167,8 +178,17 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {

<SignaturePad
id="signature"
signature={{
value: localSignature?.value ?? '',
type: localSignature?.type ?? SignatureType.DRAW,
}}
className="border-border mt-2 h-44 w-full rounded-md border"
onChange={(value) => setLocalSignature(value)}
onChange={(value: string | null, isUploaded: boolean) => {
setLocalSignature({
value,
type: isUploaded ? SignatureType.UPLOAD : SignatureType.DRAW,
});
}}
Comment on lines +186 to +191

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for complex onChange function, it's better them to define them as function and use them in JSX

/>
</div>

Expand Down
31 changes: 26 additions & 5 deletions apps/web/src/components/forms/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import { useState } from 'react';

import { useRouter } from 'next/navigation';

import { zodResolver } from '@hookform/resolvers/zod';
Expand All @@ -21,12 +23,12 @@ import {
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';

export const ZProfileFormSchema = z.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
signature: z.string().min(1, 'Signature Pad cannot be empty'),
signature: z.string(),
});

export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
Expand All @@ -40,6 +42,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
const router = useRouter();

const { toast } = useToast();
const [isUploaded, setIsUploaded] = useState(false);

const form = useForm<TProfileFormSchema>({
values: {
Expand All @@ -55,9 +58,17 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {

const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
try {
if (signature === '') {
form.setError('signature', {
type: 'manual',
message: 'Signature Pad cannot be empty',
});
return;
}
await updateProfile({
name,
signature,
signatureType: isUploaded ? 'UPLOAD' : 'DRAW',
});

toast({
Expand Down Expand Up @@ -85,6 +96,11 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
}
};

const handleSignatureChange = (signature: string | null, isUploaded: boolean) => {
setIsUploaded(isUploaded);
form.setValue('signature', signature ?? '');
};

return (
<Form {...form}>
<form
Expand Down Expand Up @@ -115,16 +131,21 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
<FormField
control={form.control}
name="signature"
render={({ field: { onChange } }) => (
render={() => (
<FormItem>
<FormLabel>Signature</FormLabel>
<FormControl>
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
containerClassName={cn('rounded-lg border bg-background')}
defaultValue={user.signature ?? undefined}
onChange={(v) => onChange(v ?? '')}
signature={{
value: user.signature,
type: user.signatureType,
}}
onChange={(v: string | null, isUploaded: boolean) =>
handleSignatureChange(v, isUploaded)
}
Comment on lines +146 to +148

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simply write

onChange={handleSignatureChange}

/>
</FormControl>
<FormMessage />
Expand Down
28 changes: 25 additions & 3 deletions apps/web/src/components/forms/signup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

import { zodResolver } from '@hookform/resolvers/zod';
Expand All @@ -9,6 +10,7 @@ import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';

import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { SignatureType } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
Expand All @@ -24,7 +26,7 @@ import {
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';

const SIGN_UP_REDIRECT_PATH = '/documents';
Expand All @@ -35,6 +37,7 @@ export const ZSignUpFormSchema = z
email: z.string().email().min(1),
password: ZPasswordSchema,
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
signatureType: z.nativeEnum(SignatureType),
})
.refine(
(data) => {
Expand All @@ -57,6 +60,7 @@ export type SignUpFormProps = {
export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
const { toast } = useToast();
const analytics = useAnalytics();
const [isUploaded, setIsUploaded] = useState(false);
const router = useRouter();

const form = useForm<TSignUpFormSchema>({
Expand All @@ -65,6 +69,7 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
email: initialEmail ?? '',
password: '',
signature: '',
signatureType: 'DRAW',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace ''DRAW' with SignatureType.DRAW

},
resolver: zodResolver(ZSignUpFormSchema),
});
Expand All @@ -75,7 +80,13 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign

const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
await signup({ name, email, password, signature });
await signup({
name,
email,
password,
signature,
signatureType: isUploaded ? SignatureType.UPLOAD : SignatureType.DRAW,
});

router.push(`/unverified-account`);

Expand Down Expand Up @@ -121,6 +132,11 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
}
};

const handleSignatureChange = (signature: string | null, isUploaded: boolean) => {
setIsUploaded(isUploaded);
form.setValue('signature', signature ?? '');
};

return (
<Form {...form}>
<form
Expand Down Expand Up @@ -181,7 +197,13 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
className="h-36 w-full"
disabled={isSubmitting}
containerClassName="mt-2 rounded-lg border bg-background"
onChange={(v) => onChange(v ?? '')}
signature={{
value: form.watch('signature'),
type: isUploaded ? 'UPLOAD' : 'DRAW',
}}
onChange={(v: string | null, isUploaded: boolean) =>
handleSignatureChange(v, isUploaded)
}
Comment on lines +204 to +206

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simply write

onChange={handleSignatureChange}

/>
</FormControl>

Expand Down
11 changes: 10 additions & 1 deletion packages/lib/server-only/user/create-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { hash } from 'bcrypt';
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
import { updateSubscriptionItemQuantity } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity';
import { prisma } from '@documenso/prisma';
import type { SignatureType } from '@documenso/prisma/client';
import { IdentityProvider, Prisma, TeamMemberInviteStatus } from '@documenso/prisma/client';

import { IS_BILLING_ENABLED } from '../../constants/app';
Expand All @@ -13,9 +14,16 @@ export interface CreateUserOptions {
email: string;
password: string;
signature?: string | null;
signatureType: SignatureType;
}

export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => {
export const createUser = async ({
name,
email,
password,
signature,
signatureType,
}: CreateUserOptions) => {
const hashedPassword = await hash(password, SALT_ROUNDS);

const userExists = await prisma.user.findFirst({
Expand All @@ -34,6 +42,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse
email: email.toLowerCase(),
password: hashedPassword,
signature,
signatureType,
identityProvider: IdentityProvider.DOCUMENSO,
},
});
Expand Down