Compare commits

...

8 Commits

Author SHA1 Message Date
7705dbae0c feat: add document log page link (#1099)
## Description

Adds a link from the document page view to the document page log view

<img width="289" alt="image"
src="https://github.com/documenso/documenso/assets/20962767/335af85a-26c3-4849-a54e-25eb62373574">
2024-04-11 15:04:36 +07:00
8b58f10cbe feat: add cta on complete page (#1028)
![CleanShot 2024-03-18 at 11 45
40](https://github.com/documenso/documenso/assets/25515812/ae3b88de-359d-4019-866a-a76097bbb0fe)
![CleanShot 2024-03-18 at 11 46
25](https://github.com/documenso/documenso/assets/25515812/b5ff7078-623e-476c-8800-17d14bc8efa9)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced a "Claim Account" feature allowing new users to sign up by
providing their name, email, and password.
- Enhanced user experience for both logged-in and non-logged-in users
with improved UI/UX and additional functionality.

- **Enhancements**
- Implemented form validation and error handling for a smoother sign-up
process.
	- Integrated analytics to track user actions during account claiming.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
Co-authored-by: David Nguyen <davidngu28@gmail.com>
2024-04-11 10:09:04 +03:00
fe1f0e6a76 fix: remove status widget for now (#1098)
- removing widget since stability fix seems somehow worse in prod
2024-04-10 18:25:56 +02:00
a82975fd78 chore: keep import until fix or complete remove 2024-04-10 18:24:32 +02:00
a4967f19e8 fix: remove status widget for now 2024-04-10 18:22:46 +02:00
a311869c9b fix: status widget rerendering (#1097)
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---

## Description

Should fix the rerendering of the status widget 
## Related Issue

<!--- If this pull request is related to a specific issue, reference it
here using #issue_number. -->
<!--- For example, "Fixes #123" or "Addresses #456". -->

## Changes Made

<!--- Provide a summary of the changes made in this pull request. -->
<!--- Include any relevant technical details or architecture changes.
-->

- Change 1
- Change 2
- ...

## Testing Performed

<!--- Describe the testing that you have performed to validate these
changes. -->
<!--- Include information about test cases, testing environments, and
results. -->

- Tested feature X in scenario Y.
- Ran unit tests for component Z.
- Tested on browsers A, B, and C.
- ...

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [ ] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous
submission, if applicable.

## Additional Notes

<!--- Provide any additional context or notes for the reviewers. -->
<!--- This might include details about design decisions, potential
concerns, or anything else relevant. -->
2024-04-10 17:52:20 +02:00
732827f81d Merge branch 'main' into main 2024-04-10 17:32:32 +02:00
f7ae3104ea fix: status widget rerendering 2024-04-10 17:05:22 +02:00
9 changed files with 292 additions and 93 deletions

View File

@ -13,7 +13,7 @@ import LogoImage from '@documenso/assets/logo.png';
import { cn } from '@documenso/ui/lib/utils';
import { ThemeSwitcher } from '@documenso/ui/primitives/theme-switcher';
import { StatusWidgetContainer } from './status-widget-container';
// import { StatusWidgetContainer } from './status-widget-container';
export type FooterProps = HTMLAttributes<HTMLDivElement>;
@ -65,9 +65,9 @@ export const Footer = ({ className, ...props }: FooterProps) => {
))}
</div>
<div className="mt-6">
{/* <div className="mt-6">
<StatusWidgetContainer />
</div>
</div> */}
</div>
<div className="grid w-full max-w-sm grid-cols-2 gap-x-4 gap-y-2 md:w-auto md:gap-x-8">

View File

@ -6,7 +6,7 @@ import { StatusWidget } from './status-widget';
export function StatusWidgetContainer() {
return (
<Suspense fallback={<StatusWidgetFallback />}>
<StatusWidget />
<StatusWidget slug="documenso-status" />
</Suspense>
);
}

View File

@ -1,7 +1,6 @@
import { use, useMemo } from 'react';
import { memo, use } from 'react';
import type { Status } from '@openstatus/react';
import { getStatus } from '@openstatus/react';
import { type Status, getStatus } from '@openstatus/react';
import { cn } from '@documenso/ui/lib/utils';
@ -45,9 +44,8 @@ const getStatusLevel = (level: Status) => {
}[level];
};
export function StatusWidget() {
const getStatusMemoized = useMemo(async () => getStatus('documenso-status'), []);
const { status } = use(getStatusMemoized);
export const StatusWidget = memo(function StatusWidget({ slug }: { slug: string }) {
const { status } = use(getStatus(slug));
const level = getStatusLevel(status);
return (
@ -72,4 +70,4 @@ export function StatusWidget() {
</span>
</a>
);
}
});

View File

@ -4,7 +4,16 @@ import { useState } from 'react';
import Link from 'next/link';
import { Copy, Download, Edit, Loader, MoreHorizontal, Share, Trash2 } from 'lucide-react';
import {
Copy,
Download,
Edit,
Loader,
MoreHorizontal,
ScrollTextIcon,
Share,
Trash2,
} from 'lucide-react';
import { useSession } from 'next-auth/react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
@ -106,6 +115,13 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
</DropdownMenuItem>
)}
<DropdownMenuItem asChild>
<Link href={`${documentsPath}/${document.id}/logs`}>
<ScrollTextIcon className="mr-2 h-4 w-4" />
Logs
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
<Copy className="mr-2 h-4 w-4" />
Duplicate

View File

@ -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<typeof ZClaimAccountFormSchema>;
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<TClaimAccountFormSchema>({
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 (
<div className="mt-2 w-full">
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset disabled={form.formState.isSubmitting} className="mt-4">
<FormField
name="name"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter your name" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="email"
control={form.control}
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>Email address</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter your email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem className="mt-4">
<FormLabel>Set a password</FormLabel>
<FormControl>
<PasswordInput {...field} placeholder="Pick a password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="mt-6 w-full" loading={form.formState.isSubmitting}>
Claim account
</Button>
</fieldset>
</form>
</Form>
</div>
);
};

View File

@ -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,9 +85,30 @@ export default async function CompletedSigningPage({
const sessionData = await getServerSession();
const isLoggedIn = !!sessionData?.user;
const canSignUp = !isLoggedIn && NEXT_PUBLIC_DISABLE_SIGNUP !== 'true';
return (
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44">
<div
className={cn(
'-mx-4 flex flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44',
{ 'pt-0 lg:pt-0 xl:pt-0': canSignUp },
)}
>
<div
className={cn('relative mt-6 flex w-full flex-col items-center justify-center', {
'mt-0 flex-col divide-y overflow-hidden pt-6 md:pt-16 lg:flex-row lg:divide-x lg:divide-y-0 lg:pt-20 xl:pt-24':
canSignUp,
})}
>
<div
className={cn('flex flex-col items-center', {
'mb-8 p-4 md:mb-0 md:p-12': canSignUp,
})}
>
<Badge variant="neutral" size="default" className="mb-6 rounded-xl border bg-transparent">
{truncatedTitle}
</Badge>
{/* Card with recipient */}
<SigningCard3D
name={recipientName}
@ -89,10 +116,16 @@ export default async function CompletedSigningPage({
signingCelebrationImage={signingCelebration}
/>
<div className="relative mt-6 flex w-full flex-col items-center">
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
Document
{recipient.role === RecipientRole.SIGNER && ' Signed '}
{recipient.role === RecipientRole.VIEWER && ' Viewed '}
{recipient.role === RecipientRole.APPROVER && ' Approved '}
</h2>
{match({ status: document.status, deletedAt: document.deletedAt })
.with({ status: DocumentStatus.COMPLETED }, () => (
<div className="text-documenso-700 flex items-center text-center">
<div className="text-documenso-700 mt-4 flex items-center text-center">
<CheckCircle2 className="mr-2 h-5 w-5" />
<span className="text-sm">Everyone has signed</span>
</div>
@ -110,14 +143,6 @@ export default async function CompletedSigningPage({
</div>
))}
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
You have
{recipient.role === RecipientRole.SIGNER && ' signed '}
{recipient.role === RecipientRole.VIEWER && ' viewed '}
{recipient.role === RecipientRole.APPROVER && ' approved '}
<span className="mt-1.5 block">"{truncatedTitle}"</span>
</h2>
{match({ status: document.status, deletedAt: document.deletedAt })
.with({ status: DocumentStatus.COMPLETED }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
@ -131,8 +156,8 @@ export default async function CompletedSigningPage({
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
This document has been cancelled by the owner and is no longer available for others to
sign.
This document has been cancelled by the owner and is no longer available for others
to sign.
</p>
))}
@ -154,21 +179,26 @@ export default async function CompletedSigningPage({
/>
)}
</div>
</div>
{isLoggedIn ? (
{canSignUp && (
<div className={`flex max-w-xl flex-col items-center justify-center p-4 md:p-12`}>
<h2 className="mt-8 text-center text-xl font-semibold md:mt-0">
Need to sign documents?
</h2>
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
Create your account and start using state-of-the-art document signing.
</p>
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
</div>
)}
{isLoggedIn && (
<Link href="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
Go Back Home
</Link>
) : (
<p className="text-muted-foreground/60 mt-36 text-sm">
Want to send slick signing links like this one?{' '}
<Link
href="https://documenso.com"
className="text-documenso-700 hover:text-documenso-600"
>
Check out Documenso.
</Link>
</p>
)}
</div>
</div>

View File

@ -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);

View File

@ -56,7 +56,7 @@ export const authRouter = router({
return user;
} catch (err) {
console.log(err);
console.error(err);
const error = AppError.parseError(err);

View File

@ -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()