Merge branch 'feat/refresh' into feat/redirect-signed-document

This commit is contained in:
Ephraim Atta-Duncan
2023-09-17 14:52:44 +00:00
87 changed files with 3476 additions and 634 deletions

View File

@ -0,0 +1,30 @@
import React from 'react';
import { redirect } from 'next/navigation';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { AdminNav } from './nav';
export type AdminSectionLayoutProps = {
children: React.ReactNode;
};
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
const user = await getRequiredServerComponentSession();
if (!isAdmin(user)) {
redirect('/documents');
}
return (
<div className="mx-auto mt-16 w-full max-w-screen-xl px-4 md:px-8">
<div className="grid grid-cols-12 gap-x-8 md:mt-8">
<AdminNav className="col-span-12 md:col-span-3 md:flex" />
<div className="col-span-12 mt-12 md:col-span-9 md:mt-0">{children}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,47 @@
'use client';
import { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { BarChart3, User2 } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
export type AdminNavProps = HTMLAttributes<HTMLDivElement>;
export const AdminNav = ({ className, ...props }: AdminNavProps) => {
const pathname = usePathname();
return (
<div className={cn('flex gap-x-2.5 gap-y-2 md:flex-col', className)} {...props}>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/stats') && 'bg-secondary',
)}
asChild
>
<Link href="/admin/stats">
<BarChart3 className="mr-2 h-5 w-5" />
Stats
</Link>
</Button>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/users') && 'bg-secondary',
)}
disabled
>
<User2 className="mr-2 h-5 w-5" />
Users (Coming Soon)
</Button>
</div>
);
};

View File

@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default function Admin() {
redirect('/admin/stats');
}

View File

@ -0,0 +1,75 @@
import {
File,
FileCheck,
FileClock,
FileEdit,
Mail,
MailOpen,
PenTool,
User as UserIcon,
UserPlus2,
UserSquare2,
} from 'lucide-react';
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import {
getUsersCount,
getUsersWithSubscriptionsCount,
} from '@documenso/lib/server-only/admin/get-users-stats';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
export default async function AdminStatsPage() {
const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([
getUsersCount(),
getUsersWithSubscriptionsCount(),
getDocumentStats(),
getRecipientsStats(),
]);
return (
<div>
<h2 className="text-4xl font-semibold">Instance Stats</h2>
<div className="mt-8 grid flex-1 grid-cols-1 gap-4 md:grid-cols-4">
<CardMetric icon={UserIcon} title="Total Users" value={usersCount} />
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
<CardMetric
icon={UserPlus2}
title="Active Subscriptions"
value={usersWithSubscriptionsCount}
/>
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
</div>
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-2">
<div>
<h3 className="text-3xl font-semibold">Document metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
</div>
</div>
<div>
<h3 className="text-3xl font-semibold">Recipients metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
<CardMetric
icon={UserSquare2}
title="Total Recipients"
value={recipientStats.TOTAL_RECIPIENTS}
/>
<CardMetric icon={Mail} title="Documents Received" value={recipientStats.SENT} />
<CardMetric icon={MailOpen} title="Documents Viewed" value={recipientStats.OPENED} />
<CardMetric icon={PenTool} title="Signatures Collected" value={recipientStats.SIGNED} />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,124 +0,0 @@
import Link from 'next/link';
import { Clock, File, FileCheck } from 'lucide-react';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@documenso/ui/primitives/table';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { UploadDocument } from './upload-document';
const CARD_DATA = [
{
icon: FileCheck,
title: 'Completed',
status: InternalDocumentStatus.COMPLETED,
},
{
icon: File,
title: 'Drafts',
status: InternalDocumentStatus.DRAFT,
},
{
icon: Clock,
title: 'Pending',
status: InternalDocumentStatus.PENDING,
},
];
export default async function DashboardPage() {
const user = await getRequiredServerComponentSession();
const [stats, results] = await Promise.all([
getStats({
user,
}),
findDocuments({
userId: user.id,
perPage: 10,
}),
]);
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<h1 className="text-4xl font-semibold">Dashboard</h1>
<div className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3">
{CARD_DATA.map((card) => (
<Link key={card.status} href={`/documents?status=${card.status}`}>
<CardMetric icon={card.icon} title={card.title} value={stats[card.status]} />
</Link>
))}
</div>
<div className="mt-12">
<UploadDocument />
<h2 className="mt-8 text-2xl font-semibold">Recent Documents</h2>
<div className="border-border mt-8 overflow-x-auto rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">ID</TableHead>
<TableHead>Title</TableHead>
<TableHead>Reciepient</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{results.data.map((document) => {
return (
<TableRow key={document.id}>
<TableCell className="font-medium">{document.id}</TableCell>
<TableCell>
<Link
href={`/documents/${document.id}`}
className="focus-visible:ring-ring ring-offset-background rounded-md font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
>
{document.title}
</Link>
</TableCell>
<TableCell>
<StackAvatarsWithTooltip recipients={document.Recipient} />
</TableCell>
<TableCell>
<DocumentStatus status={document.status} />
</TableCell>
<TableCell className="text-right">
<LocaleDate date={document.created} />
</TableCell>
</TableRow>
);
})}
{results.data.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
);
}

View File

@ -4,7 +4,8 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Document, Field, Recipient, User } from '@documenso/prisma/client';
import { Field, Recipient, User } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { AddFieldsFormPartial } from '@documenso/ui/primitives/document-flow/add-fields';
@ -28,9 +29,10 @@ import { completeDocument } from '~/components/forms/edit-document/add-subject.a
export type EditDocumentFormProps = {
className?: string;
user: User;
document: Document;
document: DocumentWithData;
recipients: Recipient[];
fields: Field[];
dataUrl: string;
};
type EditDocumentStep = 'signers' | 'fields' | 'subject';
@ -41,14 +43,13 @@ export const EditDocumentForm = ({
recipients,
fields,
user: _user,
dataUrl,
}: EditDocumentFormProps) => {
const { toast } = useToast();
const router = useRouter();
const [step, setStep] = useState<EditDocumentStep>('signers');
const documentUrl = `data:application/pdf;base64,${document.document}`;
const documentFlow: Record<EditDocumentStep, DocumentFlowStep> = {
signers: {
title: 'Add Signers',
@ -136,7 +137,7 @@ export const EditDocumentForm = ({
duration: 5000,
});
router.push('/dashboard');
router.push('/documents');
} catch (err) {
console.error(err);
@ -151,11 +152,11 @@ export const EditDocumentForm = ({
return (
<div className={cn('grid w-full grid-cols-12 gap-8', className)}>
<Card
className="col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
className="relative col-span-12 rounded-xl before:rounded-xl lg:col-span-6 xl:col-span-7"
gradient
>
<CardContent className="p-2">
<LazyPDFViewer document={documentUrl} />
<LazyPDFViewer document={dataUrl} />
</CardContent>
</Card>

View File

@ -1,20 +0,0 @@
'use client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { PDFViewerProps } from '@documenso/ui/primitives/pdf-viewer';
export type LoadablePDFCard = PDFViewerProps & {
className?: string;
pdfClassName?: string;
};
export const LoadablePDFCard = ({ className, pdfClassName, ...props }: LoadablePDFCard) => {
return (
<Card className={className} gradient {...props}>
<CardContent className="p-2">
<LazyPDFViewer className={pdfClassName} {...props} />
</CardContent>
</Card>
);
};

View File

@ -7,6 +7,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getFieldsForDocument } from '@documenso/lib/server-only/field/get-fields-for-document';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
@ -36,10 +37,16 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
userId: session.id,
}).catch(() => null);
if (!document) {
if (!document || !document.documentData) {
redirect('/documents');
}
const { documentData } = document;
const documentDataUrl = await getFile(documentData)
.then((buffer) => Buffer.from(buffer).toString('base64'))
.then((data) => `data:application/pdf;base64,${data}`);
const [recipients, fields] = await Promise.all([
await getRecipientsForDocument({
documentId,
@ -86,12 +93,13 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
user={session}
recipients={recipients}
fields={fields}
dataUrl={documentDataUrl}
/>
)}
{document.status === InternalDocumentStatus.COMPLETED && (
<div className="mx-auto mt-12 max-w-2xl">
<LazyPDFViewer document={`data:application/pdf;base64,${document.document}`} />
<LazyPDFViewer document={documentDataUrl} />
</div>
)}
</div>

View File

@ -15,7 +15,10 @@ import {
} from 'lucide-react';
import { useSession } from 'next-auth/react';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { Document, DocumentStatus, Recipient, User } from '@documenso/prisma/client';
import { DocumentWithData } from '@documenso/prisma/types/document-with-data';
import { trpc } from '@documenso/trpc/client';
import {
DropdownMenu,
DropdownMenuContent,
@ -47,17 +50,26 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
const isComplete = row.status === DocumentStatus.COMPLETED;
// const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
const onDownloadClick = () => {
let decodedDocument = row.document;
const onDownloadClick = async () => {
let document: DocumentWithData | null = null;
try {
decodedDocument = atob(decodedDocument);
} catch (err) {
// We're just going to ignore this error and try to download the document
console.error(err);
if (!recipient) {
document = await trpc.document.getDocumentById.query({
id: row.id,
});
} else {
document = await trpc.document.getDocumentByToken.query({
token: recipient.token,
});
}
const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0)));
const documentData = document?.documentData;
if (!documentData) {
return;
}
const documentBytes = await getFile(documentData);
const blob = new Blob([documentBytes], {
type: 'application/pdf',
@ -82,14 +94,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel>
<DropdownMenuItem disabled={!recipient} asChild>
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
<Link href={`/sign/${recipient?.token}`}>
<Pencil className="mr-2 h-4 w-4" />
Sign
</Link>
</DropdownMenuItem>
<DropdownMenuItem disabled={!isOwner} asChild>
<DropdownMenuItem disabled={!isOwner || isComplete} asChild>
<Link href={`/documents/${row.id}`}>
<Edit className="mr-2 h-4 w-4" />
Edit

View File

@ -0,0 +1,56 @@
'use client';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { Document, Recipient, User } from '@documenso/prisma/client';
export type DataTableTitleProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
};
};
export const DataTableTitle = ({ row }: DataTableTitleProps) => {
const { data: session } = useSession();
if (!session) {
return null;
}
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
const isRecipient = !!recipient;
return match({
isOwner,
isRecipient,
})
.with({ isOwner: true }, () => (
<Link
href={`/documents/${row.id}`}
title={row.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.title}
</Link>
))
.with({ isRecipient: true }, () => (
<Link
href={`/sign/${recipient?.token}`}
title={row.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.title}
</Link>
))
.otherwise(() => (
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">
{row.title}
</span>
));
};

View File

@ -2,9 +2,8 @@
import { useTransition } from 'react';
import Link from 'next/link';
import { Loader } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { FindResultSet } from '@documenso/lib/types/find-result-set';
@ -18,6 +17,7 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { DataTableActionButton } from './data-table-action-button';
import { DataTableActionDropdown } from './data-table-action-dropdown';
import { DataTableTitle } from './data-table-title';
export type DocumentsDataTableProps = {
results: FindResultSet<
@ -29,6 +29,7 @@ export type DocumentsDataTableProps = {
};
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
const { data: session } = useSession();
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
@ -42,25 +43,22 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
});
};
if (!session) {
return null;
}
return (
<div className="relative">
<DataTable
columns={[
{
header: 'ID',
accessorKey: 'id',
header: 'Created',
accessorKey: 'createdAt',
cell: ({ row }) => <LocaleDate date={row.original.createdAt} />,
},
{
header: 'Title',
cell: ({ row }) => (
<Link
href={`/documents/${row.original.id}`}
title={row.original.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.original.title}
</Link>
),
cell: ({ row }) => <DataTableTitle row={row.original} />,
},
{
header: 'Recipient',
@ -74,11 +72,6 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.getValue('status')} />,
},
{
header: 'Created',
accessorKey: 'created',
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
},
{
header: 'Actions',
cell: ({ row }) => (
@ -95,7 +88,7 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination table={table} />}
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (

View File

@ -11,8 +11,8 @@ import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-
import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentStatus } from '~/components/formatter/document-status';
import { UploadDocument } from '../dashboard/upload-document';
import { DocumentsDataTable } from './data-table';
import { UploadDocument } from './upload-document';
export type DocumentsPageProps = {
searchParams?: {
@ -39,7 +39,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
userId: user.id,
status,
orderBy: {
column: 'created',
column: 'createdAt',
direction: 'desc',
},
page,
@ -81,6 +81,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats[value], 99)}
{stats[value] > 99 && '+'}
</span>
)}
</Link>

View File

@ -1,29 +1,45 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCreateDocument } from '~/api/document/create/fetcher';
export type UploadDocumentProps = {
className?: string;
};
export const UploadDocument = ({ className }: UploadDocumentProps) => {
const { toast } = useToast();
const router = useRouter();
const { isLoading, mutateAsync: createDocument } = useCreateDocument();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation();
const onFileDrop = async (file: File) => {
try {
setIsLoading(true);
const { type, data } = await putFile(file);
const { id: documentDataId } = await createDocumentData({
type,
data,
});
const { id } = await createDocument({
file: file,
title: file.name,
documentDataId,
});
toast({
@ -41,6 +57,8 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => {
description: 'An error occurred while uploading your document.',
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};

View File

@ -21,16 +21,18 @@ export default async function BillingSettingsPage() {
redirect('/settings/profile');
}
let subscription = await getSubscriptionByUserId({ userId: user.id });
const subscription = await getSubscriptionByUserId({ userId: user.id }).then(async (sub) => {
if (sub) {
return sub;
}
// If we don't have a customer record, create one as well as an empty subscription.
if (!subscription?.customerId) {
subscription = await createCustomer({ user });
}
// If we don't have a customer record, create one as well as an empty subscription.
return createCustomer({ user });
});
let billingPortalUrl = '';
if (subscription?.customerId) {
if (subscription.customerId) {
billingPortalUrl = await getPortalSession({
customerId: subscription.customerId,
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`,

View File

@ -1,55 +1,64 @@
'use client';
import { HTMLAttributes } from 'react';
import { HTMLAttributes, useState } from 'react';
import { Download } from 'lucide-react';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentData } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DownloadButtonProps = HTMLAttributes<HTMLButtonElement> & {
disabled?: boolean;
fileName?: string;
document?: string;
documentData?: DocumentData;
};
export const DownloadButton = ({
className,
fileName,
document,
documentData,
disabled,
...props
}: DownloadButtonProps) => {
/**
* Convert the document from base64 to a blob and download it.
*/
const onDownloadClick = () => {
if (!document) {
return;
}
const { toast } = useToast();
let decodedDocument = document;
const [isLoading, setIsLoading] = useState(false);
const onDownloadClick = async () => {
try {
decodedDocument = atob(document);
setIsLoading(true);
if (!documentData) {
return;
}
const bytes = await getFile(documentData);
const blob = new Blob([bytes], {
type: 'application/pdf',
});
const link = window.document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = fileName || 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
} catch (err) {
// We're just going to ignore this error and try to download the document
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while downloading your document.',
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
const documentBytes = Uint8Array.from(decodedDocument.split('').map((c) => c.charCodeAt(0)));
const blob = new Blob([documentBytes], {
type: 'application/pdf',
});
const link = window.document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = fileName || 'document.pdf';
link.click();
window.URL.revokeObjectURL(link.href);
};
return (
@ -57,8 +66,9 @@ export const DownloadButton = ({
type="button"
variant="outline"
className={className}
disabled={disabled || !document}
disabled={disabled || !documentData}
onClick={onDownloadClick}
loading={isLoading}
{...props}
>
<Download className="mr-2 h-5 w-5" />

View File

@ -30,15 +30,21 @@ export default async function CompletedSigningPage({
token,
}).catch(() => null);
if (!document) {
if (!document || !document.documentData) {
return notFound();
}
const { documentData } = document;
const [fields, recipient] = await Promise.all([
getFieldsForToken({ token }),
getRecipientByToken({ token }),
getRecipientByToken({ token }).catch(() => null),
]);
if (!recipient) {
return notFound();
}
const recipientName =
recipient.name ||
fields.find((field) => field.type === FieldType.NAME)?.customText ||
@ -91,7 +97,7 @@ export default async function CompletedSigningPage({
<DownloadButton
className="flex-1"
fileName={document.title}
document={document.status === DocumentStatus.COMPLETED ? document.document : undefined}
documentData={documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
</div>

View File

@ -8,6 +8,7 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
@ -36,14 +37,22 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
token,
}).catch(() => null),
getFieldsForToken({ token }),
getRecipientByToken({ token }),
getRecipientByToken({ token }).catch(() => null),
viewedDocument({ token }),
]);
if (!document) {
if (!document || !document.documentData || !recipient) {
return notFound();
}
const { documentData } = document;
const documentDataUrl = await getFile(documentData)
.then((buffer) => Buffer.from(buffer).toString('base64'))
.then((data) => `data:application/pdf;base64,${data}`);
const user = await getServerComponentSession();
if (
document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED
@ -51,10 +60,6 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
redirect(`/sign/${token}/complete`);
}
const user = await getServerComponentSession();
const documentUrl = `data:application/pdf;base64,${document.document}`;
return (
<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">
@ -74,7 +79,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
gradient
>
<CardContent className="p-2">
<LazyPDFViewer document={documentUrl} />
<LazyPDFViewer document={documentDataUrl} />
</CardContent>
</Card>

View File

@ -2,6 +2,8 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster';
@ -45,6 +47,8 @@ export const metadata = {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getServerComponentAllFlags();
const locale = getLocale();
return (
<html
lang="en"
@ -63,16 +67,18 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</Suspense>
<body>
<FeatureFlagProvider initialFlags={flags}>
<PlausibleProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>{children}</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
</PlausibleProvider>
<Toaster />
</FeatureFlagProvider>
<LocaleProvider locale={locale}>
<FeatureFlagProvider initialFlags={flags}>
<PlausibleProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>{children}</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
</PlausibleProvider>
<Toaster />
</FeatureFlagProvider>
</LocaleProvider>
</body>
</html>
);