mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
@ -43,9 +43,10 @@ type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
|
|||||||
export interface RejectDocumentDialogProps {
|
export interface RejectDocumentDialogProps {
|
||||||
document: Pick<Document, 'id'>;
|
document: Pick<Document, 'id'>;
|
||||||
token: string;
|
token: string;
|
||||||
|
onRejected?: (reason: string) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) {
|
export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@ -79,7 +80,11 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr
|
|||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
|
||||||
router.push(`/sign/${token}/rejected`);
|
if (onRejected) {
|
||||||
|
await onRejected(reason);
|
||||||
|
} else {
|
||||||
|
router.push(`/sign/${token}/rejected`);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
|||||||
40
apps/web/src/app/embed/rejected.tsx
Normal file
40
apps/web/src/app/embed/rejected.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Trans } from '@lingui/macro';
|
||||||
|
import { XCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { Signature } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
export type EmbedDocumentRejectedPageProps = {
|
||||||
|
name?: string;
|
||||||
|
signature?: Signature;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbedDocumentRejected = ({ name }: EmbedDocumentRejectedPageProps) => {
|
||||||
|
return (
|
||||||
|
<div className="embed--DocumentRejected relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -10,7 +10,13 @@ import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'
|
|||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||||
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
import type { DocumentMeta, TemplateMeta } from '@documenso/prisma/client';
|
||||||
import { type DocumentData, type Field, FieldType, RecipientRole } from '@documenso/prisma/client';
|
import {
|
||||||
|
type DocumentData,
|
||||||
|
type Field,
|
||||||
|
FieldType,
|
||||||
|
RecipientRole,
|
||||||
|
SigningStatus,
|
||||||
|
} from '@documenso/prisma/client';
|
||||||
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
@ -26,11 +32,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
|
||||||
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
import { RecipientProvider } from '~/app/(signing)/sign/[token]/recipient-context';
|
||||||
|
import { RejectDocumentDialog } from '~/app/(signing)/sign/[token]/reject-document-dialog';
|
||||||
import { Logo } from '~/components/branding/logo';
|
import { Logo } from '~/components/branding/logo';
|
||||||
|
|
||||||
import { EmbedClientLoading } from '../../client-loading';
|
import { EmbedClientLoading } from '../../client-loading';
|
||||||
import { EmbedDocumentCompleted } from '../../completed';
|
import { EmbedDocumentCompleted } from '../../completed';
|
||||||
import { EmbedDocumentFields } from '../../document-fields';
|
import { EmbedDocumentFields } from '../../document-fields';
|
||||||
|
import { EmbedDocumentRejected } from '../../rejected';
|
||||||
import { injectCss } from '../../util';
|
import { injectCss } from '../../util';
|
||||||
import { ZSignDocumentEmbedDataSchema } from './schema';
|
import { ZSignDocumentEmbedDataSchema } from './schema';
|
||||||
|
|
||||||
@ -75,6 +83,9 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted);
|
||||||
|
const [hasRejectedDocument, setHasRejectedDocument] = useState(
|
||||||
|
recipient.signingStatus === SigningStatus.REJECTED,
|
||||||
|
);
|
||||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(
|
||||||
allRecipients.length > 0 ? allRecipients[0].id : null,
|
allRecipients.length > 0 ? allRecipients[0].id : null,
|
||||||
);
|
);
|
||||||
@ -83,6 +94,8 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
const [isNameLocked, setIsNameLocked] = useState(false);
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
|
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||||
|
|
||||||
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId);
|
||||||
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
const isAssistantMode = recipient.role === RecipientRole.ASSISTANT;
|
||||||
|
|
||||||
@ -161,6 +174,25 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDocumentRejected = (reason: string) => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-rejected',
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
documentId,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
reason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasRejectedDocument(true);
|
||||||
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const hash = window.location.hash.slice(1);
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
@ -174,6 +206,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
// Since a recipient can be provided a name we can lock it without requiring
|
// Since a recipient can be provided a name we can lock it without requiring
|
||||||
// a to be provided by the parent application, unlike direct templates.
|
// a to be provided by the parent application, unlike direct templates.
|
||||||
setIsNameLocked(!!data.lockName);
|
setIsNameLocked(!!data.lockName);
|
||||||
|
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
||||||
|
|
||||||
if (data.darkModeDisabled) {
|
if (data.darkModeDisabled) {
|
||||||
document.documentElement.classList.add('dark-mode-disabled');
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
@ -208,6 +241,10 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
}
|
}
|
||||||
}, [hasFinishedInit, hasDocumentLoaded]);
|
}, [hasFinishedInit, hasDocumentLoaded]);
|
||||||
|
|
||||||
|
if (hasRejectedDocument) {
|
||||||
|
return <EmbedDocumentRejected name={fullName} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasCompletedDocument) {
|
if (hasCompletedDocument) {
|
||||||
return (
|
return (
|
||||||
<EmbedDocumentCompleted
|
<EmbedDocumentCompleted
|
||||||
@ -229,6 +266,16 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
<div className="embed--Root relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
|
||||||
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
|
||||||
|
|
||||||
|
{allowDocumentRejection && (
|
||||||
|
<div className="embed--Actions mb-4 flex w-full flex-row-reverse items-baseline justify-between">
|
||||||
|
<RejectDocumentDialog
|
||||||
|
document={{ id: documentId }}
|
||||||
|
token={token}
|
||||||
|
onRejected={onDocumentRejected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
<div className="embed--DocumentContainer relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="embed--DocumentViewer flex-1">
|
<div className="embed--DocumentViewer flex-1">
|
||||||
@ -420,7 +467,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className="col-start-2"
|
className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
|
||||||
disabled={
|
disabled={
|
||||||
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,4 +13,5 @@ export const ZSignDocumentEmbedDataSchema = ZBaseEmbedDataSchema.extend({
|
|||||||
.optional()
|
.optional()
|
||||||
.transform((value) => value || undefined),
|
.transform((value) => value || undefined),
|
||||||
lockName: z.boolean().optional().default(false),
|
lockName: z.boolean().optional().default(false),
|
||||||
|
allowDocumentRejection: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { jobs } from '../../jobs/client';
|
import { jobs } from '../../jobs/client';
|
||||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||||
import {
|
import {
|
||||||
@ -72,6 +73,13 @@ export const completeDocumentWithToken = async ({
|
|||||||
throw new Error(`Recipient ${recipient.id} has already signed`);
|
throw new Error(`Recipient ${recipient.id} has already signed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (recipient.signingStatus === SigningStatus.REJECTED) {
|
||||||
|
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||||
|
message: 'Recipient has already rejected the document',
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user