Compare commits
32 Commits
feat/add-d
...
feat/blog-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24036b0f24 | ||
|
|
975d52a07e | ||
|
|
f8a193c0f8 | ||
|
|
9186cb4d7b | ||
|
|
933076fa3f | ||
|
|
abc91f7eac | ||
|
|
35acf05997 | ||
|
|
ff957a2f82 | ||
|
|
6640f0496a | ||
|
|
de3ebe16ee | ||
|
|
84a2d3baf6 | ||
|
|
74180defd1 | ||
|
|
aeeaaf0d8d | ||
|
|
2b84293c4e | ||
|
|
b38ef6c0a7 | ||
|
|
17af4d25bd | ||
|
|
6e095921e6 | ||
|
|
150c42b246 | ||
|
|
aecf2f32b9 | ||
|
|
b23967d777 | ||
|
|
b3291c65bc | ||
|
|
4b849e286c | ||
|
|
7bcc26a987 | ||
|
|
692722d32e | ||
|
|
e4f06d8e30 | ||
|
|
c799380787 | ||
|
|
5540fcf0d2 | ||
|
|
d9da09c1e7 | ||
|
|
fe90aa3b7b | ||
|
|
0c680e0111 | ||
|
|
7bcf5fbd86 | ||
|
|
7218b950fe |
20
.devcontainer/devcontainer.json
Normal file
20
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "Documenso",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||||
|
"version": "latest",
|
||||||
|
"enableNonRootDocker": "true",
|
||||||
|
"moby": "true"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {}
|
||||||
|
},
|
||||||
|
"onCreateCommand": "./.devcontainer/on-create.sh",
|
||||||
|
"forwardPorts": [
|
||||||
|
3000,
|
||||||
|
54320,
|
||||||
|
9000,
|
||||||
|
2500,
|
||||||
|
1100
|
||||||
|
]
|
||||||
|
}
|
||||||
18
.devcontainer/on-create.sh
Executable file
18
.devcontainer/on-create.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Start the database and mailserver
|
||||||
|
docker compose -f ./docker/compose-without-app.yml up -d
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Copy the env file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Source the env file, export the variables
|
||||||
|
set -a
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
# Run the migrations
|
||||||
|
npm run -w @documenso/prisma prisma:migrate-dev
|
||||||
3
.devcontainer/post-start.sh
Executable file
3
.devcontainer/post-start.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
npm run dev
|
||||||
@@ -12,7 +12,7 @@ tags:
|
|||||||
|
|
||||||
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
|
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
|
||||||
|
|
||||||
Last week, Lucas shared the reasoning how [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
|
Last week, Lucas shared the reasoning on [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
|
||||||
|
|
||||||
Today, I'm pleased to share with you a preview of the next Documenso.
|
Today, I'm pleased to share with you a preview of the next Documenso.
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
title: Design System
|
|
||||||
---
|
|
||||||
|
|
||||||
# We're building a beautiful, open-source alternative to DocuSign
|
|
||||||
|
|
||||||
> Read more about our design culture here:
|
|
||||||
>
|
|
||||||
> [https://documenso.com/blog/design-system](https://documenso.com/blog/design-system)
|
|
||||||
|
|
||||||
At Documenso, we aim to be a design-driven company.
|
|
||||||
|
|
||||||
We believe that design isn't just about how things look, but also how they work. We want to make sure that the product is easy to use and intuitive. We also want to ensure that the website, desktop and mobile apps are consistent and look and feel like they belong together.
|
|
||||||
|
|
||||||
To achieve this, we've created Documenso's design system containing tokens, primitives, and components, screens, and brand assets.
|
|
||||||
|
|
||||||
We're open-sourcing this design system so you can see how we build the product and think about design as a whole.
|
|
||||||
|
|
||||||
## Check out the design system
|
|
||||||
|
|
||||||
<iframe
|
|
||||||
src="https://documen.so/design-system-embed"
|
|
||||||
className="aspect-square w-full border-none"
|
|
||||||
frameBorder="0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
## Remix and Share the community version on Figma
|
|
||||||
|
|
||||||
<a href="documen.so/design" target="_blank">
|
|
||||||
<figure>
|
|
||||||
<MdxNextImage
|
|
||||||
src="/blog/designsystem.png"
|
|
||||||
width="1260"
|
|
||||||
height="630"
|
|
||||||
alt="Documenso's Design System"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<figcaption className="text-center">
|
|
||||||
Documenso's Design System ✨
|
|
||||||
</figcaption>
|
|
||||||
</figure>
|
|
||||||
</a>
|
|
||||||
@@ -21,7 +21,6 @@ const FOOTER_LINKS = [
|
|||||||
{ href: '/open', text: 'Open' },
|
{ href: '/open', text: 'Open' },
|
||||||
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
|
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
|
||||||
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
|
||||||
{ href: '/design-system', text: 'Design' },
|
|
||||||
{ href: 'mailto:support@documenso.com', text: 'Support' },
|
{ href: 'mailto:support@documenso.com', text: 'Support' },
|
||||||
{ href: '/privacy', text: 'Privacy' },
|
{ href: '/privacy', text: 'Privacy' },
|
||||||
];
|
];
|
||||||
@@ -44,7 +43,7 @@ export const Footer = ({ className, ...props }: FooterProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid max-w-xs flex-1 grid-cols-2 gap-x-4 gap-y-2">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5">
|
||||||
{FOOTER_LINKS.map((link, index) => (
|
{FOOTER_LINKS.map((link, index) => (
|
||||||
<Link
|
<Link
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export const MENU_NAVIGATION_LINKS = [
|
|||||||
href: '/pricing',
|
href: '/pricing',
|
||||||
text: 'Pricing',
|
text: 'Pricing',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/open',
|
||||||
|
text: 'Open',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: 'https://status.documenso.com',
|
href: 'https://status.documenso.com',
|
||||||
text: 'Status',
|
text: 'Status',
|
||||||
@@ -59,7 +63,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
transition={{
|
transition={{
|
||||||
staggerChildren: 0.2,
|
staggerChildren: 0.03,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{MENU_NAVIGATION_LINKS.map(({ href, text }) => (
|
{MENU_NAVIGATION_LINKS.map(({ href, text }) => (
|
||||||
@@ -75,6 +79,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
x: 0,
|
x: 0,
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
|
ease: 'backInOut',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -82,14 +82,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuItem disabled={!recipient} asChild>
|
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
||||||
<Link href={`/sign/${recipient?.token}`}>
|
<Link href={`/sign/${recipient?.token}`}>
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
Sign
|
Sign
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem disabled={!isOwner} asChild>
|
<DropdownMenuItem disabled={!isOwner || isComplete} asChild>
|
||||||
<Link href={`/documents/${row.id}`}>
|
<Link href={`/documents/${row.id}`}>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
|
|||||||
96
apps/web/src/app/(signing)/sign/[token]/email-field.tsx
Normal file
96
apps/web/src/app/(signing)/sign/[token]/email-field.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useTransition } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { Loader } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Recipient } from '@documenso/prisma/client';
|
||||||
|
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useRequiredSigningContext } from './provider';
|
||||||
|
import { SigningFieldContainer } from './signing-field-container';
|
||||||
|
|
||||||
|
export type EmailFieldProps = {
|
||||||
|
field: FieldWithSignature;
|
||||||
|
recipient: Recipient;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmailField = ({ field, recipient }: EmailFieldProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { email: providedEmail } = 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 onSign = async () => {
|
||||||
|
try {
|
||||||
|
await signFieldWithToken({
|
||||||
|
token: recipient.token,
|
||||||
|
fieldId: field.id,
|
||||||
|
value: providedEmail ?? '',
|
||||||
|
isBase64: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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}>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="bg-background absolute inset-0 flex items-center justify-center">
|
||||||
|
<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">Email</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{field.inserted && <p className="text-muted-foreground duration-200">{field.customText}</p>}
|
||||||
|
</SigningFieldContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
|
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
@@ -13,6 +14,7 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
|||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||||
|
|
||||||
import { DateField } from './date-field';
|
import { DateField } from './date-field';
|
||||||
|
import { EmailField } from './email-field';
|
||||||
import { SigningForm } from './form';
|
import { SigningForm } from './form';
|
||||||
import { NameField } from './name-field';
|
import { NameField } from './name-field';
|
||||||
import { SigningProvider } from './provider';
|
import { SigningProvider } from './provider';
|
||||||
@@ -42,10 +44,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await getServerComponentSession();
|
||||||
|
|
||||||
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
const documentUrl = `data:application/pdf;base64,${document.document}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigningProvider email={recipient.email} fullName={recipient.name}>
|
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
|
||||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
|
||||||
{document.title}
|
{document.title}
|
||||||
@@ -84,6 +88,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
.with(FieldType.DATE, () => (
|
.with(FieldType.DATE, () => (
|
||||||
<DateField key={field.id} field={field} recipient={recipient} />
|
<DateField key={field.id} field={field} recipient={recipient} />
|
||||||
))
|
))
|
||||||
|
.with(FieldType.EMAIL, () => (
|
||||||
|
<EmailField key={field.id} field={field} recipient={recipient} />
|
||||||
|
))
|
||||||
.otherwise(() => null),
|
.otherwise(() => null),
|
||||||
)}
|
)}
|
||||||
</ElementVisible>
|
</ElementVisible>
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ export const useRequiredSigningContext = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface SigningProviderProps {
|
export interface SigningProviderProps {
|
||||||
fullName?: string;
|
fullName?: string | null;
|
||||||
email?: string;
|
email?: string | null;
|
||||||
signature?: string;
|
signature?: string | null;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
} = useForm<TProfileFormSchema>({
|
} = useForm<TProfileFormSchema>({
|
||||||
values: {
|
values: {
|
||||||
name: user.name ?? '',
|
name: user.name ?? '',
|
||||||
signature: '',
|
signature: user.signature || '',
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZProfileFormSchema),
|
resolver: zodResolver(ZProfileFormSchema),
|
||||||
});
|
});
|
||||||
@@ -118,6 +118,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
render={({ field: { onChange } }) => (
|
render={({ field: { onChange } }) => (
|
||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full rounded-lg border bg-white backdrop-blur-sm dark:border-[#e2d7c5] dark:bg-[#fcf8ee]"
|
className="h-44 w-full rounded-lg border bg-white backdrop-blur-sm dark:border-[#e2d7c5] dark:bg-[#fcf8ee]"
|
||||||
|
defaultValue={user.signature ?? undefined}
|
||||||
onChange={(v) => onChange(v ?? '')}
|
onChange={(v) => onChange(v ?? '')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
@@ -7,12 +11,20 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { FcGoogle } from 'react-icons/fc';
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
const ErrorMessages = {
|
||||||
|
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
|
||||||
|
[ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
|
||||||
|
[ErrorCode.USER_MISSING_PASSWORD]:
|
||||||
|
'This account appears to be using a social login method, please sign in using that method',
|
||||||
|
};
|
||||||
|
|
||||||
export const ZSignInFormSchema = z.object({
|
export const ZSignInFormSchema = z.object({
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
password: z.string().min(6).max(72),
|
password: z.string().min(6).max(72),
|
||||||
@@ -26,6 +38,7 @@ export type SignInFormProps = {
|
|||||||
|
|
||||||
export const SignInForm = ({ className }: SignInFormProps) => {
|
export const SignInForm = ({ className }: SignInFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -39,6 +52,27 @@ export const SignInForm = ({ className }: SignInFormProps) => {
|
|||||||
resolver: zodResolver(ZSignInFormSchema),
|
resolver: zodResolver(ZSignInFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const errorCode = searchParams?.get('error');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
if (isErrorCode(errorCode)) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
description: ErrorMessages[errorCode] ?? 'An unknown error occurred',
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [errorCode, toast]);
|
||||||
|
|
||||||
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
|
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
import { TRPCClientError } from '@documenso/trpc/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
@@ -19,6 +20,7 @@ export const ZSignUpFormSchema = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email().min(1),
|
email: z.string().email().min(1),
|
||||||
password: z.string().min(6).max(72),
|
password: z.string().min(6).max(72),
|
||||||
|
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
|
||||||
@@ -31,6 +33,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
control,
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
@@ -39,15 +42,16 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
signature: '',
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZSignUpFormSchema),
|
resolver: zodResolver(ZSignUpFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, email, password }: TSignUpFormSchema) => {
|
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await signup({ name, email, password });
|
await signup({ name, email, password, signature });
|
||||||
|
|
||||||
await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
email,
|
email,
|
||||||
@@ -119,8 +123,19 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
|
|||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SignaturePad className="mt-2 h-36 w-full rounded-lg border bg-white dark:border-[#e2d7c5] dark:bg-[#fcf8ee]" />
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signature"
|
||||||
|
render={({ field: { onChange } }) => (
|
||||||
|
<SignaturePad
|
||||||
|
className="mt-2 h-36 w-full rounded-lg border bg-white dark:border-[#e2d7c5] dark:bg-[#fcf8ee]"
|
||||||
|
onChange={(v) => onChange(v ?? '')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormErrorMessage className="mt-1.5" error={errors.signature} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90">
|
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --filter=@documenso/{web,marketing}",
|
"dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
|
||||||
"start": "cd apps && cd web && next start",
|
"start": "cd apps && cd web && next start",
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
import { getUserByEmail } from '../server-only/user/get-user-by-email';
|
||||||
import { ErrorCodes } from './error-codes';
|
import { ErrorCode } from './error-codes';
|
||||||
|
|
||||||
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
||||||
adapter: PrismaAdapter(prisma),
|
adapter: PrismaAdapter(prisma),
|
||||||
@@ -24,23 +24,23 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
|
|||||||
},
|
},
|
||||||
authorize: async (credentials, _req) => {
|
authorize: async (credentials, _req) => {
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
throw new Error(ErrorCodes.CredentialsNotFound);
|
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, password } = credentials;
|
const { email, password } = credentials;
|
||||||
|
|
||||||
const user = await getUserByEmail({ email }).catch(() => {
|
const user = await getUserByEmail({ email }).catch(() => {
|
||||||
throw new Error(ErrorCodes.IncorrectEmailPassword);
|
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user.password) {
|
if (!user.password) {
|
||||||
throw new Error(ErrorCodes.UserMissingPassword);
|
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPasswordsSame = await compare(password, user.password);
|
const isPasswordsSame = await compare(password, user.password);
|
||||||
|
|
||||||
if (!isPasswordsSame) {
|
if (!isPasswordsSame) {
|
||||||
throw new Error(ErrorCodes.IncorrectEmailPassword);
|
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export const ErrorCodes = {
|
export const isErrorCode = (code: unknown): code is ErrorCode => {
|
||||||
IncorrectEmailPassword: 'incorrect-email-password',
|
return typeof code === 'string' && code in ErrorCode;
|
||||||
UserMissingPassword: 'missing-password',
|
};
|
||||||
CredentialsNotFound: 'credentials-not-found',
|
|
||||||
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
||||||
|
|
||||||
|
export const ErrorCode = {
|
||||||
|
INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
|
||||||
|
USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
|
||||||
|
CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -83,10 +83,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('documents', documents);
|
|
||||||
|
|
||||||
if (documents.count > 0) {
|
if (documents.count > 0) {
|
||||||
console.log('sealing document');
|
|
||||||
await sealDocument({ documentId: document.id });
|
await sealDocument({ documentId: document.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,10 +53,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
|
|||||||
const doc = await PDFDocument.load(pdfData);
|
const doc = await PDFDocument.load(pdfData);
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
console.log('inserting field', {
|
|
||||||
...field,
|
|
||||||
Signature: null,
|
|
||||||
});
|
|
||||||
await insertFieldInPDF(doc, field);
|
await insertFieldInPDF(doc, field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
const fieldX = pageWidth * (Number(field.positionX) / 100);
|
const fieldX = pageWidth * (Number(field.positionX) / 100);
|
||||||
const fieldY = pageHeight * (Number(field.positionY) / 100);
|
const fieldY = pageHeight * (Number(field.positionY) / 100);
|
||||||
|
|
||||||
console.log({
|
|
||||||
fieldWidth,
|
|
||||||
fieldHeight,
|
|
||||||
fieldX,
|
|
||||||
fieldY,
|
|
||||||
pageWidth,
|
|
||||||
pageHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
const font = await pdf.embedFont(isSignatureField ? fontCaveat : StandardFonts.Helvetica);
|
const font = await pdf.embedFont(isSignatureField ? fontCaveat : StandardFonts.Helvetica);
|
||||||
|
|
||||||
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
|
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
|
||||||
@@ -75,15 +66,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||||
imageY = pageHeight - imageY - imageHeight;
|
imageY = pageHeight - imageY - imageHeight;
|
||||||
|
|
||||||
console.log({
|
|
||||||
initialDimensions,
|
|
||||||
scalingFactor,
|
|
||||||
imageWidth,
|
|
||||||
imageHeight,
|
|
||||||
imageX,
|
|
||||||
imageY,
|
|
||||||
});
|
|
||||||
|
|
||||||
page.drawImage(image, {
|
page.drawImage(image, {
|
||||||
x: imageX,
|
x: imageX,
|
||||||
y: imageY,
|
y: imageY,
|
||||||
@@ -107,17 +89,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
const textX = fieldX + (fieldWidth - textWidth) / 2;
|
const textX = fieldX + (fieldWidth - textWidth) / 2;
|
||||||
let textY = fieldY + (fieldHeight - textHeight) / 2;
|
let textY = fieldY + (fieldHeight - textHeight) / 2;
|
||||||
|
|
||||||
console.log({
|
|
||||||
initialDimensions,
|
|
||||||
scalingFactor,
|
|
||||||
textWidth,
|
|
||||||
textHeight,
|
|
||||||
textX,
|
|
||||||
textY,
|
|
||||||
pageWidth,
|
|
||||||
pageHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
// Invert the Y axis since PDFs use a bottom-left coordinate system
|
||||||
textY = pageHeight - textY - textHeight;
|
textY = pageHeight - textY - textHeight;
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ export interface CreateUserOptions {
|
|||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
signature?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createUser = async ({ name, email, password }: CreateUserOptions) => {
|
export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => {
|
||||||
const hashedPassword = await hash(password, SALT_ROUNDS);
|
const hashedPassword = await hash(password, SALT_ROUNDS);
|
||||||
|
|
||||||
const userExists = await prisma.user.findFirst({
|
const userExists = await prisma.user.findFirst({
|
||||||
@@ -29,6 +30,7 @@ export const createUser = async ({ name, email, password }: CreateUserOptions) =
|
|||||||
name,
|
name,
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
signature,
|
||||||
identityProvider: IdentityProvider.DOCUMENSO,
|
identityProvider: IdentityProvider.DOCUMENSO,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ export type UpdateProfileOptions = {
|
|||||||
signature: string;
|
signature: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateProfile = async ({
|
export const updateProfile = async ({ userId, name, signature }: UpdateProfileOptions) => {
|
||||||
userId,
|
|
||||||
name,
|
|
||||||
// TODO: Actually use signature
|
|
||||||
signature: _signature,
|
|
||||||
}: UpdateProfileOptions) => {
|
|
||||||
// Existence check
|
// Existence check
|
||||||
await prisma.user.findFirstOrThrow({
|
await prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@@ -25,7 +20,7 @@ export const updateProfile = async ({
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
// signature,
|
signature,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "signature" TEXT;
|
||||||
@@ -20,6 +20,7 @@ model User {
|
|||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
password String?
|
password String?
|
||||||
source String?
|
source String?
|
||||||
|
signature String?
|
||||||
identityProvider IdentityProvider @default(DOCUMENSO)
|
identityProvider IdentityProvider @default(DOCUMENSO)
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const { fontFamily } = require('tailwindcss/defaultTheme');
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: ['class'],
|
darkMode: ['class'],
|
||||||
content: ['src/**/*.{ts,tsx}', 'content/**/*.{md,mdx}'],
|
content: ['src/**/*.{ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { ZSignUpMutationSchema } from './schema';
|
|||||||
export const authRouter = router({
|
export const authRouter = router({
|
||||||
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
|
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const { name, email, password } = input;
|
const { name, email, password, signature } = input;
|
||||||
|
|
||||||
return await createUser({ name, email, password });
|
return await createUser({ name, email, password, signature });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const ZSignUpMutationSchema = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(6),
|
password: z.string().min(6),
|
||||||
|
signature: z.string().min(1, { message: 'A signature is required.' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSignUpMutationSchema = z.infer<typeof ZSignUpMutationSchema>;
|
export type TSignUpMutationSchema = z.infer<typeof ZSignUpMutationSchema>;
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
|
|
||||||
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
|
||||||
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
|
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
|
||||||
|
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
|
||||||
|
|
||||||
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
|
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
|
||||||
|
|
||||||
@@ -314,7 +315,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{!hideRecipients && (
|
{!hideRecipients && (
|
||||||
<Popover>
|
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -324,7 +325,7 @@ export const AddFieldsFormPartial = ({
|
|||||||
>
|
>
|
||||||
{selectedSigner?.email && (
|
{selectedSigner?.email && (
|
||||||
<span className="flex-1 truncate text-left">
|
<span className="flex-1 truncate text-left">
|
||||||
{selectedSigner?.email} ({selectedSigner?.email})
|
{selectedSigner?.name} ({selectedSigner?.email})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -348,7 +349,10 @@ export const AddFieldsFormPartial = ({
|
|||||||
className={cn({
|
className={cn({
|
||||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||||
})}
|
})}
|
||||||
onSelect={() => setSelectedSigner(recipient)}
|
onSelect={() => {
|
||||||
|
setSelectedSigner(recipient);
|
||||||
|
setShowRecipientsSelector(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{recipient.sendStatus !== SendStatus.SENT ? (
|
{recipient.sendStatus !== SendStatus.SENT ? (
|
||||||
<Check
|
<Check
|
||||||
|
|||||||
Reference in New Issue
Block a user