mirror of
https://github.com/documenso/documenso.git
synced 2025-11-23 21:21:37 +10:00
fix: merge conflicts
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user