diff --git a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx index 547a346d8..110fa8f99 100644 --- a/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/reject-document-dialog.tsx @@ -43,9 +43,10 @@ type TRejectDocumentFormSchema = z.infer; export interface RejectDocumentDialogProps { document: Pick; token: string; + onRejected?: (reason: string) => void | Promise; } -export function RejectDocumentDialog({ document, token }: RejectDocumentDialogProps) { +export function RejectDocumentDialog({ document, token, onRejected }: RejectDocumentDialogProps) { const { toast } = useToast(); const router = useRouter(); const searchParams = useSearchParams(); @@ -79,7 +80,11 @@ export function RejectDocumentDialog({ document, token }: RejectDocumentDialogPr setIsOpen(false); - router.push(`/sign/${token}/rejected`); + if (onRejected) { + await onRejected(reason); + } else { + router.push(`/sign/${token}/rejected`); + } } catch (err) { toast({ title: 'Error', diff --git a/apps/web/src/app/embed/rejected.tsx b/apps/web/src/app/embed/rejected.tsx new file mode 100644 index 000000000..deb8bd026 --- /dev/null +++ b/apps/web/src/app/embed/rejected.tsx @@ -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 ( +
+
+
+ + +

+ Document Rejected +

+
+ +
+ You have rejected this document +
+ +

+ + The document owner has been notified of your decision. They may contact you with further + instructions if necessary. + +

+ +

+ No further action is required from you at this time. +

+
+
+ ); +}; diff --git a/apps/web/src/app/embed/sign/[[...url]]/client.tsx b/apps/web/src/app/embed/sign/[[...url]]/client.tsx index e8ee6ad52..f7635ddab 100644 --- a/apps/web/src/app/embed/sign/[[...url]]/client.tsx +++ b/apps/web/src/app/embed/sign/[[...url]]/client.tsx @@ -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 { validateFieldsInserted } from '@documenso/lib/utils/fields'; 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 { trpc } from '@documenso/trpc/react'; 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 { 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 { EmbedClientLoading } from '../../client-loading'; import { EmbedDocumentCompleted } from '../../completed'; import { EmbedDocumentFields } from '../../document-fields'; +import { EmbedDocumentRejected } from '../../rejected'; import { injectCss } from '../../util'; import { ZSignDocumentEmbedDataSchema } from './schema'; @@ -75,6 +83,9 @@ export const EmbedSignDocumentClientPage = ({ const [hasFinishedInit, setHasFinishedInit] = useState(false); const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false); const [hasCompletedDocument, setHasCompletedDocument] = useState(isCompleted); + const [hasRejectedDocument, setHasRejectedDocument] = useState( + recipient.signingStatus === SigningStatus.REJECTED, + ); const [selectedSignerId, setSelectedSignerId] = useState( allRecipients.length > 0 ? allRecipients[0].id : null, ); @@ -83,6 +94,8 @@ export const EmbedSignDocumentClientPage = ({ const [isNameLocked, setIsNameLocked] = useState(false); const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false); + const [allowDocumentRejection, setAllowDocumentRejection] = useState(false); + const selectedSigner = allRecipients.find((r) => r.id === selectedSignerId); 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(() => { 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 // a to be provided by the parent application, unlike direct templates. setIsNameLocked(!!data.lockName); + setAllowDocumentRejection(!!data.allowDocumentRejection); if (data.darkModeDisabled) { document.documentElement.classList.add('dark-mode-disabled'); @@ -208,6 +241,10 @@ export const EmbedSignDocumentClientPage = ({ } }, [hasFinishedInit, hasDocumentLoaded]); + if (hasRejectedDocument) { + return ; + } + if (hasCompletedDocument) { return ( {(!hasFinishedInit || !hasDocumentLoaded) && } + {allowDocumentRejection && ( +
+ +
+ )} +
{/* Viewer */}
@@ -420,7 +467,7 @@ export const EmbedSignDocumentClientPage = ({ ) : (