feat: migrate nextjs to rr7

This commit is contained in:
David Nguyen
2025-01-02 15:33:37 +11:00
parent 9183f668d3
commit 383b5f78f0
898 changed files with 31175 additions and 24615 deletions

View File

@ -0,0 +1,71 @@
import { Trans } from '@lingui/react/macro';
import { ChevronLeft } from 'lucide-react';
import { Link, Outlet } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { Button } from '@documenso/ui/primitives/button';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
import type { Route } from './+types/_layout';
export function loader() {
const session = getOptionalLoaderSession();
return {
user: session?.user,
teams: session?.teams || [],
};
}
/**
* A layout to handle scenarios where the user is a recipient of a given resource
* where we do not care whether they are authenticated or not.
*
* Such as direct template access, or signing.
*/
export default function RecipientLayout({ loaderData }: Route.ComponentProps) {
const { user, teams } = loaderData;
return (
<div className="min-h-screen">
{user && <AuthenticatedHeader user={user} teams={teams} />}
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">
<Outlet />
</main>
</div>
);
}
export function ErrorBoundary() {
return (
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
<div>
<p className="text-muted-foreground font-semibold">
<Trans>404 Not found</Trans>
</p>
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
<Trans>Oops! Something went wrong.</Trans>
</h1>
<p className="text-muted-foreground mt-4 text-sm">
<Trans>
The resource you are looking for may have been disabled, deleted or may have never
existed.
</Trans>
</p>
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
<Button asChild className="w-32">
<Link to="/">
<ChevronLeft className="mr-2 h-4 w-4" />
<Trans>Go Back</Trans>
</Link>
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,111 @@
import { Plural } from '@lingui/react/macro';
import { UsersIcon } from 'lucide-react';
import { redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DirectTemplatePageView } from '~/components/general/direct-template/direct-template-page';
import { DirectTemplateAuthPageView } from '~/components/general/direct-template/direct-template-signing-auth-page';
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/_index';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
const { token } = params;
if (!token) {
throw redirect('/');
}
const template = await getTemplateByDirectLinkToken({
token,
}).catch(() => null);
if (!template || !template.directLink?.enabled) {
throw new Response('Not Found', { status: 404 });
}
const directTemplateRecipient = template.recipients.find(
(recipient) => recipient.id === template.directLink?.directTemplateRecipientId,
);
if (!directTemplateRecipient) {
throw new Response('Not Found', { status: 404 });
}
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: template.authOptions,
});
// Ensure typesafety when we add more options.
const isAccessAuthValid = match(derivedRecipientAccessAuth)
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session?.user))
.with(null, () => true)
.exhaustive();
if (!isAccessAuthValid) {
return superLoaderJson({
isAccessAuthValid: false as const,
});
}
return superLoaderJson({
isAccessAuthValid: true,
template,
directTemplateRecipient,
} as const);
}
export default function DirectTemplatePage() {
const { user } = useOptionalSession();
const data = useSuperLoaderData<typeof loader>();
// Should not be possible for directLink to be null.
if (!data.isAccessAuthValid) {
return <DirectTemplateAuthPageView />;
}
const { template, directTemplateRecipient } = data;
return (
<DocumentSigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
<DocumentSigningAuthProvider
documentAuthOptions={template.authOptions}
recipient={directTemplateRecipient}
user={user}
>
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={template.title}
>
{template.title}
</h1>
<div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2">
<UsersIcon className="h-4 w-4" />
<p className="text-muted-foreground/80">
<Plural value={template.recipients.length} one="# recipient" other="# recipients" />
</p>
</div>
<DirectTemplatePageView
directTemplateRecipient={directTemplateRecipient}
directTemplateToken={template.directLink.token}
template={template}
/>
</div>
</DocumentSigningAuthProvider>
</DocumentSigningProvider>
);
}

View File

@ -0,0 +1,230 @@
import { Trans } from '@lingui/react/macro';
import { DocumentStatus, SigningStatus } from '@prisma/client';
import { Clock8 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
import { DocumentSigningPageView } from '~/components/general/document-signing/document-signing-page-view';
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/_index';
export async function loader({ params }: Route.LoaderArgs) {
const { session, requestMetadata } = getOptionalLoaderContext();
const { token } = params;
if (!token) {
throw new Response('Not Found', { status: 404 });
}
const user = session?.user;
const [document, fields, recipient, completedFields] = await Promise.all([
getDocumentAndSenderByToken({
token,
userId: user?.id,
requireAccessAuth: false,
}).catch(() => null),
getFieldsForToken({ token }),
getRecipientByToken({ token }).catch(() => null),
getCompletedFieldsForToken({ token }),
]);
if (
!document ||
!document.documentData ||
!recipient ||
document.status === DocumentStatus.DRAFT
) {
throw new Response('Not Found', { status: 404 });
}
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurn) {
throw redirect(`/sign/${token}/waiting`);
}
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: document.authOptions,
recipientAuth: recipient.authOptions,
});
const isDocumentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
documentAuthOptions: document.authOptions,
recipient,
userId: user?.id,
});
let recipientHasAccount: boolean | null = null;
if (!isDocumentAccessValid) {
recipientHasAccount = await getUserByEmail({ email: recipient.email })
.then((user) => !!user)
.catch(() => false);
return superLoaderJson({
isDocumentAccessValid: false,
recipientEmail: recipient.email,
recipientHasAccount,
} as const);
}
await viewedDocument({
token,
requestMetadata,
recipientAccessAuth: derivedRecipientAccessAuth,
}).catch(() => null);
const { documentMeta } = document;
if (recipient.signingStatus === SigningStatus.REJECTED) {
throw redirect(`/sign/${token}/rejected`);
}
if (
document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED
) {
throw redirect(documentMeta?.redirectUrl || `/sign/${token}/complete`);
}
// Todo: We don't handle encrypted files right.
// if (documentMeta?.password) {
// const key = DOCUMENSO_ENCRYPTION_KEY;
// if (!key) {
// throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
// }
// const securePassword = Buffer.from(
// symmetricDecrypt({
// key,
// data: documentMeta.password,
// }),
// ).toString('utf-8');
// documentMeta.password = securePassword;
// }
const [recipientSignature] = await getRecipientSignatures({ recipientId: recipient.id });
return superLoaderJson({
isDocumentAccessValid: true,
document,
fields,
recipient,
completedFields,
recipientSignature,
isRecipientsTurn,
} as const);
}
export default function SigningPage() {
const data = useSuperLoaderData<typeof loader>();
const { user } = useOptionalSession();
if (!data.isDocumentAccessValid) {
return (
<DocumentSigningAuthPageView
email={data.recipientEmail}
emailHasAccount={!!data.recipientHasAccount}
/>
);
}
const { document, fields, recipient, completedFields, recipientSignature, isRecipientsTurn } =
data;
if (document.deletedAt) {
return (
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-16 md:-mx-8 md:px-8 lg:pt-16 xl:pt-24">
<SigningCard3D
name={recipient.name}
signature={recipientSignature}
signingCelebrationImage={signingCelebration}
/>
<div className="relative mt-2 flex w-full flex-col items-center">
<div className="mt-8 flex items-center text-center text-red-600">
<Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">
<Trans>Document Cancelled</Trans>
</span>
</div>
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
<Trans>
<span className="mt-1.5 block">"{document.title}"</span>
is no longer available to sign
</Trans>
</h2>
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<Trans>This document has been cancelled by the owner.</Trans>
</p>
{user ? (
<Link to="/documents" className="text-documenso-700 hover:text-documenso-600 mt-36">
<Trans>Go Back Home</Trans>
</Link>
) : (
<p className="text-muted-foreground/60 mt-36 text-sm">
<Trans>
Want to send slick signing links like this one?{' '}
<Link
to="https://documenso.com"
className="text-documenso-700 hover:text-documenso-600"
>
Check out Documenso.
</Link>
</Trans>
</p>
)}
</div>
</div>
);
}
return (
<DocumentSigningProvider
email={recipient.email}
fullName={user?.email === recipient.email ? user?.name : recipient.name}
signature={user?.email === recipient.email ? user?.signature : undefined}
>
<DocumentSigningAuthProvider
documentAuthOptions={document.authOptions}
recipient={recipient}
user={user}
>
<DocumentSigningPageView
recipient={recipient}
document={document}
fields={fields}
completedFields={completedFields}
isRecipientsTurn={isRecipientsTurn}
/>
</DocumentSigningAuthProvider>
</DocumentSigningProvider>
);
}

View File

@ -0,0 +1,287 @@
import { useEffect } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type Document, DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
import { Link, useRevalidator } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { match } from 'ts-pattern';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
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 { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { env } from '@documenso/lib/utils/env';
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
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 { Button } from '@documenso/ui/primitives/button';
import { ClaimAccount } from '~/components/general/claim-account';
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
import type { Route } from './+types/complete';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
const { token } = params;
if (!token) {
throw new Response('Not Found', { status: 404 });
}
const user = session?.user;
const document = await getDocumentAndSenderByToken({
token,
requireAccessAuth: false,
}).catch(() => null);
if (!document || !document.documentData) {
throw new Response('Not Found', { status: 404 });
}
const [fields, recipient] = await Promise.all([
getFieldsForToken({ token }),
getRecipientByToken({ token }).catch(() => null),
]);
if (!recipient) {
throw new Response('Not Found', { status: 404 });
}
const isDocumentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
documentAuthOptions: document.authOptions,
recipient,
userId: user?.id,
});
if (!isDocumentAccessValid) {
return {
isDocumentAccessValid: false,
recipientEmail: recipient.email,
} as const;
}
const signatures = await getRecipientSignatures({ recipientId: recipient.id });
const isExistingUser = await getUserByEmail({ email: recipient.email })
.then((u) => !!u)
.catch(() => false);
const recipientName =
recipient.name ||
fields.find((field) => field.type === FieldType.NAME)?.customText ||
recipient.email;
const canSignUp = !isExistingUser && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true';
return {
isDocumentAccessValid: true,
canSignUp,
recipientName,
recipientEmail: recipient.email,
signatures,
document,
recipient,
};
}
export default function CompletedSigningPage({ loaderData }: Route.ComponentProps) {
const { _ } = useLingui();
const { user } = useOptionalSession();
const {
isDocumentAccessValid,
canSignUp,
recipientName,
signatures,
document,
recipient,
recipientEmail,
} = loaderData;
if (!isDocumentAccessValid) {
return <DocumentSigningAuthPageView email={recipientEmail} />;
}
return (
<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">
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">
{document.title}
</span>
</Badge>
{/* Card with recipient */}
<SigningCard3D
name={recipientName}
signature={signatures.at(0)}
signingCelebrationImage={signingCelebration}
/>
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
{recipient.role === RecipientRole.SIGNER && <Trans>Document Signed</Trans>}
{recipient.role === RecipientRole.VIEWER && <Trans>Document Viewed</Trans>}
{recipient.role === RecipientRole.APPROVER && <Trans>Document Approved</Trans>}
</h2>
{match({ status: document.status, deletedAt: document.deletedAt })
.with({ status: DocumentStatus.COMPLETED }, () => (
<div className="text-documenso-700 mt-4 flex items-center text-center">
<CheckCircle2 className="mr-2 h-5 w-5" />
<span className="text-sm">
<Trans>Everyone has signed</Trans>
</span>
</div>
))
.with({ deletedAt: null }, () => (
<div className="mt-4 flex items-center text-center text-blue-600">
<Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">
<Trans>Waiting for others to sign</Trans>
</span>
</div>
))
.otherwise(() => (
<div className="flex items-center text-center text-red-600">
<Clock8 className="mr-2 h-5 w-5" />
<span className="text-sm">
<Trans>Document no longer available to sign</Trans>
</span>
</div>
))}
{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">
<Trans>
Everyone has signed! You will receive an Email copy of the signed document.
</Trans>
</p>
))
.with({ deletedAt: null }, () => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<Trans>
You will receive an Email copy of the signed document once everyone has signed.
</Trans>
</p>
))
.otherwise(() => (
<p className="text-muted-foreground/60 mt-2.5 max-w-[60ch] text-center text-sm font-medium md:text-base">
<Trans>
This document has been cancelled by the owner and is no longer available for
others to sign.
</Trans>
</p>
))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<DocumentShareButton documentId={document.id} token={recipient.token} />
{document.status === DocumentStatus.COMPLETED ? (
<DocumentDownloadButton
className="flex-1"
fileName={document.title}
documentData={document.documentData}
disabled={document.status !== DocumentStatus.COMPLETED}
/>
) : (
<DocumentDialog
documentData={document.documentData}
trigger={
<Button
className="text-[11px]"
title={_(msg`Signatures will appear once the document has been completed`)}
variant="outline"
>
<FileSearch className="mr-2 h-5 w-5" strokeWidth={1.7} />
<Trans>View Original Document</Trans>
</Button>
}
/>
)}
</div>
</div>
<div className="flex flex-col items-center">
{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">
<Trans>Need to sign documents?</Trans>
</h2>
<p className="text-muted-foreground/60 mt-4 max-w-[55ch] text-center leading-normal">
<Trans>
Create your account and start using state-of-the-art document signing.
</Trans>
</p>
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
</div>
)}
{user && (
<Link to="/documents" className="text-documenso-700 hover:text-documenso-600 mt-2">
<Trans>Go Back Home</Trans>
</Link>
)}
</div>
</div>
<PollUntilDocumentCompleted document={document} />
</div>
);
}
export type PollUntilDocumentCompletedProps = {
document: Pick<Document, 'id' | 'status' | 'deletedAt'>;
};
export const PollUntilDocumentCompleted = ({ document }: PollUntilDocumentCompletedProps) => {
const { revalidate } = useRevalidator();
useEffect(() => {
if (document.status === DocumentStatus.COMPLETED) {
return;
}
const interval = setInterval(() => {
if (window.document.hasFocus()) {
void revalidate();
}
}, 5000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [document.status]);
return <></>;
};

View File

@ -0,0 +1,125 @@
import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import { XCircle } from 'lucide-react';
import { Link } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
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 { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button';
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
import { truncateTitle } from '~/utils/truncate-title';
import type { Route } from './+types/rejected';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
const { token } = params;
if (!token) {
throw new Response('Not Found', { status: 404 });
}
const user = session?.user;
const document = await getDocumentAndSenderByToken({
token,
requireAccessAuth: false,
}).catch(() => null);
if (!document) {
throw new Response('Not Found', { status: 404 });
}
const truncatedTitle = truncateTitle(document.title);
const [fields, recipient] = await Promise.all([
getFieldsForToken({ token }),
getRecipientByToken({ token }).catch(() => null),
]);
if (!recipient) {
throw new Response('Not Found', { status: 404 });
}
const isDocumentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
documentAuthOptions: document.authOptions,
recipient,
userId: user?.id,
});
const recipientReference =
recipient.name ||
fields.find((field) => field.type === FieldType.NAME)?.customText ||
recipient.email;
if (isDocumentAccessValid) {
return {
isDocumentAccessValid: true,
recipientReference,
truncatedTitle,
};
}
// Don't leak data if access is denied.
return {
isDocumentAccessValid: false,
recipientReference,
};
}
export default function RejectedSigningPage({ loaderData }: Route.ComponentProps) {
const { user } = useOptionalSession();
const { isDocumentAccessValid, recipientReference, truncatedTitle } = loaderData;
if (!isDocumentAccessValid) {
return <DocumentSigningAuthPageView email={recipientReference} />;
}
return (
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44">
<Badge variant="neutral" size="default" className="mb-6 rounded-xl border bg-transparent">
{truncatedTitle}
</Badge>
<div className="flex flex-col items-center">
<div className="flex items-center gap-x-4">
<XCircle className="text-destructive h-10 w-10" />
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
<Trans>Document Rejected</Trans>
</h2>
</div>
<div className="text-destructive mt-4 flex items-center text-center text-sm">
<Trans>You have rejected this document</Trans>
</div>
<p className="text-muted-foreground mt-6 max-w-[60ch] text-center text-sm">
<Trans>
The document owner has been notified of your decision. They may contact you with further
instructions if necessary.
</Trans>
</p>
<p className="text-muted-foreground mt-2 max-w-[60ch] text-center text-sm">
<Trans>No further action is required from you at this time.</Trans>
</p>
{user && (
<Button className="mt-6" asChild>
<Link to={`/`}>Return Home</Link>
</Button>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,106 @@
import { Trans } from '@lingui/react/macro';
import type { Team } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { Link, redirect } from 'react-router';
import { getOptionalLoaderSession } from 'server/utils/get-loader-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { Button } from '@documenso/ui/primitives/button';
import type { Route } from './+types/waiting';
export async function loader({ params }: Route.LoaderArgs) {
const session = getOptionalLoaderSession();
const { token } = params;
if (!token) {
throw new Response('Not Found', { status: 404 });
}
const [document, recipient] = await Promise.all([
getDocumentAndSenderByToken({ token }).catch(() => null),
getRecipientByToken({ token }).catch(() => null),
]);
if (!document || !recipient) {
throw new Response('Not Found', { status: 404 });
}
if (document.status === DocumentStatus.COMPLETED) {
throw redirect(`/sign/${token}/complete`);
}
let isOwnerOrTeamMember = false;
const user = session?.user;
let team: Team | null = null;
if (user) {
isOwnerOrTeamMember = await getDocumentById({
documentId: document.id,
userId: user.id,
teamId: document.teamId ?? undefined,
})
.then((document) => !!document)
.catch(() => false);
if (document.teamId) {
team = await getTeamById({
userId: user.id,
teamId: document.teamId,
});
}
}
const documentPathForEditing = isOwnerOrTeamMember
? formatDocumentsPath(team?.url) + '/' + document.id
: null;
return {
documentPathForEditing,
};
}
export default function WaitingForTurnToSignPage({ loaderData }: Route.ComponentProps) {
const { documentPathForEditing } = loaderData;
return (
<div className="relative flex flex-col items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
<div className="w-full max-w-md text-center">
<h2 className="tracking-tigh text-3xl font-bold">
<Trans>Waiting for Your Turn</Trans>
</h2>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>
It's currently not your turn to sign. You will receive an email with instructions once
it's your turn to sign the document.
</Trans>
</p>
<p className="text-muted-foreground mt-4 text-sm">
<Trans>Please check your email for updates.</Trans>
</p>
<div className="mt-4">
{documentPathForEditing ? (
<Button variant="link" asChild>
<Link to={documentPathForEditing}>
<Trans>Were you trying to edit this document instead?</Trans>
</Link>
</Button>
) : (
<Button variant="link" asChild>
<Link to="/documents">Return Home</Link>
</Button>
)}
</div>
</div>
</div>
);
}