diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx new file mode 100644 index 000000000..b5f7c0ca8 --- /dev/null +++ b/apps/web/src/app/(signing)/sign/[token]/complete/claim-account.tsx @@ -0,0 +1,155 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; +import { TRPCClientError } from '@documenso/trpc/client'; +import { trpc } from '@documenso/trpc/react'; +import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type ClaimAccountProps = { + defaultName: string; + defaultEmail: string; + trigger?: React.ReactNode; +}; + +export const ZClaimAccountFormSchema = z + .object({ + name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), + email: z.string().email().min(1), + password: ZPasswordSchema, + }) + .refine( + (data) => { + const { name, email, password } = data; + return !password.includes(name) && !password.includes(email.split('@')[0]); + }, + { + message: 'Password should not be common or based on personal information', + path: ['password'], + }, + ); + +export type TClaimAccountFormSchema = z.infer; + +export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) => { + const analytics = useAnalytics(); + const { toast } = useToast(); + const router = useRouter(); + + const { mutateAsync: signup } = trpc.auth.signup.useMutation(); + + const form = useForm({ + values: { + name: defaultName ?? '', + email: defaultEmail, + password: '', + }, + resolver: zodResolver(ZClaimAccountFormSchema), + }); + + const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => { + try { + await signup({ name, email, password }); + + router.push(`/unverified-account`); + + toast({ + title: 'Registration Successful', + description: + 'You have successfully registered. Please verify your account by clicking on the link you received in the email.', + duration: 5000, + }); + + analytics.capture('App: User Claim Account', { + email, + timestamp: new Date().toISOString(), + }); + } catch (error) { + if (error instanceof TRPCClientError && error.data?.code === 'BAD_REQUEST') { + toast({ + title: 'An error occurred', + description: error.message, + variant: 'destructive', + }); + } else { + toast({ + title: 'An unknown error occurred', + description: + 'We encountered an unknown error while attempting to sign you up. Please try again later.', + variant: 'destructive', + }); + } + } + }; + + return ( +
+
+ +
+ ( + + Name + + + + + + )} + /> + ( + + Email address + + + + + + )} + /> + ( + + Set a password + + + + + + )} + /> + + +
+
+ +
+ ); +}; diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index c13d8636b..cfed976e5 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -3,6 +3,7 @@ import { notFound } from 'next/navigation'; import { CheckCircle2, Clock8 } from 'lucide-react'; import { getServerSession } from 'next-auth'; +import { env } from 'next-runtime-env'; import { match } from 'ts-pattern'; import signingCelebration from '@documenso/assets/images/signing-celebration.png'; @@ -16,10 +17,13 @@ import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/clie import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { SigningCard3D } from '@documenso/ui/components/signing-card'; +import { cn } from '@documenso/ui/lib/utils'; +import { Badge } from '@documenso/ui/primitives/badge'; import { truncateTitle } from '~/helpers/truncate-title'; import { SigningAuthPageView } from '../signing-auth-page'; +import { ClaimAccount } from './claim-account'; import { DocumentPreviewButton } from './document-preview-button'; export type CompletedSigningPageProps = { @@ -31,6 +35,8 @@ export type CompletedSigningPageProps = { export default async function CompletedSigningPage({ params: { token }, }: CompletedSigningPageProps) { + const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP'); + if (!token) { return notFound(); } @@ -79,96 +85,120 @@ export default async function CompletedSigningPage({ const sessionData = await getServerSession(); const isLoggedIn = !!sessionData?.user; + const canSignUp = !isLoggedIn && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true'; return ( -
- {/* Card with recipient */} - +
+
+
+ + {truncatedTitle} + -
- {match({ status: document.status, deletedAt: document.deletedAt }) - .with({ status: DocumentStatus.COMPLETED }, () => ( -
- - Everyone has signed -
- )) - .with({ deletedAt: null }, () => ( -
- - Waiting for others to sign -
- )) - .otherwise(() => ( -
- - Document no longer available to sign -
- ))} + {/* Card with recipient */} + -

- You have - {recipient.role === RecipientRole.SIGNER && ' signed '} - {recipient.role === RecipientRole.VIEWER && ' viewed '} - {recipient.role === RecipientRole.APPROVER && ' approved '} - "{truncatedTitle}" -

+

+ Document + {recipient.role === RecipientRole.SIGNER && ' Signed '} + {recipient.role === RecipientRole.VIEWER && ' Viewed '} + {recipient.role === RecipientRole.APPROVER && ' Approved '} +

- {match({ status: document.status, deletedAt: document.deletedAt }) - .with({ status: DocumentStatus.COMPLETED }, () => ( -

- Everyone has signed! You will receive an Email copy of the signed document. -

- )) - .with({ deletedAt: null }, () => ( -

- You will receive an Email copy of the signed document once everyone has signed. -

- )) - .otherwise(() => ( -

- This document has been cancelled by the owner and is no longer available for others to - sign. -

- ))} + {match({ status: document.status, deletedAt: document.deletedAt }) + .with({ status: DocumentStatus.COMPLETED }, () => ( +
+ + Everyone has signed +
+ )) + .with({ deletedAt: null }, () => ( +
+ + Waiting for others to sign +
+ )) + .otherwise(() => ( +
+ + Document no longer available to sign +
+ ))} -
- + {match({ status: document.status, deletedAt: document.deletedAt }) + .with({ status: DocumentStatus.COMPLETED }, () => ( +

+ Everyone has signed! You will receive an Email copy of the signed document. +

+ )) + .with({ deletedAt: null }, () => ( +

+ You will receive an Email copy of the signed document once everyone has signed. +

+ )) + .otherwise(() => ( +

+ This document has been cancelled by the owner and is no longer available for others + to sign. +

+ ))} - {document.status === DocumentStatus.COMPLETED ? ( - - ) : ( - - )} +
+ + + {document.status === DocumentStatus.COMPLETED ? ( + + ) : ( + + )} +
- {isLoggedIn ? ( + {canSignUp && ( +
+

+ Need to sign documents? +

+ +

+ Create your account and start using state-of-the-art document signing. +

+ + +
+ )} + + {isLoggedIn && ( Go Back Home - ) : ( -

- Want to send slick signing links like this one?{' '} - - Check out Documenso. - -

)}
diff --git a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts index ee6b160cc..c2ae0618c 100644 --- a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts +++ b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts @@ -254,7 +254,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', asyn await page.getByRole('button', { name: 'Sign' }).click(); await page.waitForURL(`/sign/${token}/complete`); - await expect(page.getByText('You have signed')).toBeVisible(); + await expect(page.getByText('Document Signed')).toBeVisible(); // Check if document has been signed const { status: completedStatus } = await getDocumentByToken(token); diff --git a/packages/trpc/server/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts index f9a1795d7..645690905 100644 --- a/packages/trpc/server/auth-router/router.ts +++ b/packages/trpc/server/auth-router/router.ts @@ -56,7 +56,7 @@ export const authRouter = router({ return user; } catch (err) { - console.log(err); + console.error(err); const error = AppError.parseError(err); diff --git a/packages/trpc/server/auth-router/schema.ts b/packages/trpc/server/auth-router/schema.ts index b84c5e1c9..71734d734 100644 --- a/packages/trpc/server/auth-router/schema.ts +++ b/packages/trpc/server/auth-router/schema.ts @@ -23,7 +23,7 @@ export const ZSignUpMutationSchema = z.object({ name: z.string().min(1), email: z.string().email(), password: ZPasswordSchema, - signature: z.string().min(1, { message: 'A signature is required.' }), + signature: z.string().nullish(), url: z .string() .trim()