fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-10-20 14:54:42 +00:00
448 changed files with 33524 additions and 9229 deletions

View File

@ -3,6 +3,7 @@ import { ChevronLeft } from 'lucide-react';
import { Link, Outlet, isRouteErrorResponse } from 'react-router';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
@ -16,14 +17,23 @@ import type { Route } from './+types/_layout';
*
* Such as direct template access, or signing.
*/
export default function RecipientLayout() {
export default function RecipientLayout({ matches }: Route.ComponentProps) {
const { sessionData } = useOptionalSession();
// Hide the header for signing routes.
const hideHeader = matches.some(
(match) => match?.id === 'routes/_recipient+/sign.$token+/_index',
);
return (
<div className="min-h-screen">
{sessionData?.user && <AuthenticatedHeader />}
{!hideHeader && sessionData?.user && <AuthenticatedHeader />}
<main className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">
<main
className={cn({
'mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8': !hideHeader,
})}
>
<Outlet />
</main>
</div>

View File

@ -7,9 +7,13 @@ import { match } from 'ts-pattern';
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getEnvelopeForRecipientSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
import { getEnvelopeRequiredAccessData } from '@documenso/lib/server-only/envelope/get-envelope-required-access-data';
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 { expireRecipient } from '@documenso/lib/server-only/recipient/expire-recipient';
@ -23,17 +27,21 @@ import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-emai
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { isRecipientExpired } from '@documenso/lib/utils/expiry';
import { prisma } from '@documenso/prisma';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
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 { DocumentSigningPageViewV1 } from '~/components/general/document-signing/document-signing-page-view-v1';
import { DocumentSigningPageViewV2 } from '~/components/general/document-signing/document-signing-page-view-v2';
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
import { EnvelopeSigningProvider } from '~/components/general/document-signing/envelope-signing-provider';
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/_index';
export async function loader({ params, request }: Route.LoaderArgs) {
const handleV1Loader = async ({ params, request }: Route.LoaderArgs) => {
const { requestMetadata } = getOptionalLoaderContext();
const { user } = await getOptionalSession(request);
@ -115,11 +123,11 @@ export async function loader({ params, request }: Route.LoaderArgs) {
.then((user) => !!user)
.catch(() => false);
return superLoaderJson({
return {
isDocumentAccessValid: false,
recipientEmail: recipient.email,
recipientHasAccount,
} as const);
} as const;
}
await viewedDocument({
@ -152,7 +160,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
const settings = await getTeamSettings({ teamId: document.teamId });
return superLoaderJson({
return {
isDocumentAccessValid: true,
document,
fields,
@ -163,13 +171,159 @@ export async function loader({ params, request }: Route.LoaderArgs) {
recipientSignature,
isRecipientsTurn,
includeSenderDetails: settings.includeSenderDetails,
} as const;
};
const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
const { token } = params;
const { requestMetadata } = getOptionalLoaderContext();
const { user } = await getOptionalSession(request);
const envelopeForSigning = await getEnvelopeForRecipientSigning({
token,
userId: user?.id,
})
.then((envelopeForSigning) => {
return {
isDocumentAccessValid: true,
...envelopeForSigning,
} as const;
})
.catch(async (e) => {
const error = AppError.parseError(e);
if (error.code === AppErrorCode.UNAUTHORIZED) {
const requiredAccessData = await getEnvelopeRequiredAccessData({ token });
return {
isDocumentAccessValid: false,
...requiredAccessData,
} as const;
}
throw new Response('Not Found', { status: 404 });
});
if (!envelopeForSigning.isDocumentAccessValid) {
return envelopeForSigning;
}
const { envelope, recipient, isCompleted, isRejected, isRecipientsTurn } = envelopeForSigning;
if (!isRecipientsTurn) {
throw redirect(`/sign/${token}/waiting`);
}
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
recipientAuth: recipient.authOptions,
});
const isAccessAuthValid = derivedRecipientAccessAuth.every((accesssAuth) =>
match(accesssAuth)
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true) // Allow without account requirement
.exhaustive(),
);
let recipientHasAccount: boolean | null = null;
if (!isAccessAuthValid) {
recipientHasAccount = await getUserByEmail({ email: recipient.email })
.then((user) => !!user)
.catch(() => false);
return {
isDocumentAccessValid: false,
recipientEmail: recipient.email,
recipientHasAccount,
} as const;
}
await viewedDocument({
token,
requestMetadata,
recipientAccessAuth: derivedRecipientAccessAuth,
}).catch(() => null);
if (isRecipientExpired(recipient)) {
const expiredRecipient = await expireRecipient({ recipientId: recipient.id });
if (expiredRecipient) {
throw redirect(`/sign/${token}/expired`);
}
}
if (isRejected) {
throw redirect(`/sign/${token}/rejected`);
}
if (isCompleted) {
throw redirect(envelope.documentMeta.redirectUrl || `/sign/${token}/complete`);
}
return {
isDocumentAccessValid: true,
envelopeForSigning,
} as const;
};
export async function loader(loaderArgs: Route.LoaderArgs) {
const { token } = loaderArgs.params;
if (!token) {
throw new Response('Not Found', { status: 404 });
}
// Not efficient but works for now until we remove v1.
const foundRecipient = await prisma.recipient.findFirst({
where: {
token,
},
select: {
envelope: {
select: {
internalVersion: true,
},
},
},
});
if (!foundRecipient) {
throw new Response('Not Found', { status: 404 });
}
if (foundRecipient.envelope.internalVersion === 2) {
const payloadV2 = await handleV2Loader(loaderArgs);
return superLoaderJson({
version: 2,
payload: payloadV2,
} as const);
}
const payloadV1 = await handleV1Loader(loaderArgs);
return superLoaderJson({
version: 1,
payload: payloadV1,
} as const);
}
export default function SigningPage() {
const data = useSuperLoaderData<typeof loader>();
if (data.version === 2) {
return <SigningPageV2 data={data.payload} />;
}
return <SigningPageV1 data={data.payload} />;
}
const SigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV1Loader>> }) => {
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
if (!data.isDocumentAccessValid) {
@ -257,16 +411,107 @@ export default function SigningPage() {
recipient={recipient}
user={user}
>
<DocumentSigningPageView
recipient={recipientWithFields}
document={document}
fields={fields}
completedFields={completedFields}
isRecipientsTurn={isRecipientsTurn}
allRecipients={allRecipients}
includeSenderDetails={includeSenderDetails}
/>
<>
{sessionData?.user && <AuthenticatedHeader />}
<div className="mb-8 mt-8 px-4 md:mb-12 md:mt-12 md:px-8">
<DocumentSigningPageViewV1
recipient={recipientWithFields}
document={document}
fields={fields}
completedFields={completedFields}
isRecipientsTurn={isRecipientsTurn}
allRecipients={allRecipients}
includeSenderDetails={includeSenderDetails}
/>
</div>
</>
</DocumentSigningAuthProvider>
</DocumentSigningProvider>
);
}
};
const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loader>> }) => {
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
if (!data.isDocumentAccessValid) {
return (
<DocumentSigningAuthPageView
email={data.recipientEmail}
emailHasAccount={!!data.recipientHasAccount}
/>
);
}
const { envelope, recipientSignature, recipient } = data.envelopeForSigning;
if (envelope.deletedAt || envelope.status === DocumentStatus.REJECTED) {
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 || undefined}
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">"{envelope.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="/" 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 (
<EnvelopeSigningProvider
envelopeData={data.envelopeForSigning}
email={recipient.email}
fullName={user?.email === recipient.email ? user?.name : recipient.name}
signature={user?.email === recipient.email ? user?.signature : undefined}
>
<DocumentSigningAuthProvider
documentAuthOptions={envelope.authOptions}
recipient={recipient}
user={user}
>
<EnvelopeRenderProvider envelope={envelope}>
<DocumentSigningPageViewV2 />
</EnvelopeRenderProvider>
</DocumentSigningAuthProvider>
</EnvelopeSigningProvider>
);
};

View File

@ -3,7 +3,7 @@ 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 { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
import { Link, useRevalidator } from 'react-router';
import { match } from 'ts-pattern';
@ -19,6 +19,7 @@ import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { env } from '@documenso/lib/utils/env';
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
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';

View File

@ -1,11 +1,11 @@
import { Trans } from '@lingui/react/macro';
import type { Team } from '@prisma/client';
import { DocumentStatus } from '@prisma/client';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { Link, redirect } from 'react-router';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-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 { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
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';
@ -40,12 +40,16 @@ export async function loader({ params, request }: Route.LoaderArgs) {
let team: Team | null = null;
if (user) {
isOwnerOrTeamMember = await getDocumentById({
documentId: document.id,
isOwnerOrTeamMember = await getEnvelopeById({
id: {
type: 'documentId',
id: document.id,
},
type: EnvelopeType.DOCUMENT,
userId: user.id,
teamId: document.teamId ?? undefined,
})
.then((document) => !!document)
.then((envelope) => !!envelope)
.catch(() => false);
if (document.teamId) {