This commit is contained in:
David Nguyen
2026-02-19 11:36:36 +11:00
parent 3acc029fef
commit e2b3597c36
11 changed files with 2162 additions and 305 deletions
@@ -9,6 +9,7 @@ export type EnvelopeItemTitleInputProps = {
className?: string;
placeholder?: string;
disabled?: boolean;
dataTestId?: string;
};
export const EnvelopeItemTitleInput = ({
@@ -17,6 +18,7 @@ export const EnvelopeItemTitleInput = ({
className,
placeholder,
disabled,
dataTestId,
}: EnvelopeItemTitleInputProps) => {
const [envelopeItemTitle, setEnvelopeItemTitle] = useState(value);
const [isError, setIsError] = useState(false);
@@ -63,6 +65,7 @@ export const EnvelopeItemTitleInput = ({
{envelopeItemTitle || placeholder}
</span>
<input
data-testid={dataTestId}
data-1p-ignore
autoComplete="off"
ref={inputRef}
@@ -72,7 +75,7 @@ export const EnvelopeItemTitleInput = ({
disabled={disabled}
style={{ width: `${inputWidth}px` }}
className={cn(
'text-foreground hover:outline-muted-foreground focus:outline-muted-foreground rounded-sm border-0 bg-transparent p-1 text-sm font-medium outline-none hover:outline hover:outline-1 focus:outline focus:outline-1',
'rounded-sm border-0 bg-transparent p-1 text-sm font-medium text-foreground outline-none hover:outline hover:outline-1 hover:outline-muted-foreground focus:outline focus:outline-1 focus:outline-muted-foreground',
className,
{
'outline-red-500': isError,
@@ -108,7 +108,7 @@ export const EnvelopeEditorUploadPage = () => {
);
const onFileDrop = async (files: File[]) => {
const newUploadingFiles: (LocalFile & { file: File; data: ArrayBuffer | null })[] =
const newUploadingFiles: (LocalFile & { file: File; data: Uint8Array<ArrayBuffer> | null })[] =
await Promise.all(
files.map(async (file) => {
return {
@@ -118,7 +118,7 @@ export const EnvelopeEditorUploadPage = () => {
file,
isUploading: isEmbedded ? false : true,
// Clone the buffer so it can be read multiple times (File.arrayBuffer() consumes the stream once)
data: isEmbedded ? (await file.arrayBuffer()).slice(0) : null,
data: isEmbedded ? new Uint8Array((await file.arrayBuffer()).slice(0)) : null,
isError: false,
};
}),
@@ -226,6 +226,30 @@ export const EnvelopeEditorUploadPage = () => {
};
const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => {
if (isEmbedded) {
const nextEnvelopeItems = files
.filter((item) => item.envelopeItemId)
.map((item, index) => {
const originalEnvelopeItem = envelope.envelopeItems.find(
(envelopeItem) => envelopeItem.id === item.envelopeItemId,
);
return {
id: item.envelopeItemId || '',
title: item.title,
order: index + 1,
envelopeId: envelope.id,
data: originalEnvelopeItem?.data,
};
});
setLocalEnvelope({
envelopeItems: nextEnvelopeItems,
});
return;
}
void updateEnvelopeItems({
envelopeId: envelope.id,
data: files
@@ -310,6 +334,7 @@ export const EnvelopeEditorUploadPage = () => {
<CardContent>
{uploadConfig?.allowUpload && (
<DocumentDropzone
data-testid="envelope-item-dropzone"
onDrop={onFileDrop}
allowMultiple
className="pb-4 pt-6"
@@ -326,7 +351,12 @@ export const EnvelopeEditorUploadPage = () => {
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="files">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} className="space-y-2">
<div
data-testid="envelope-items-list"
{...provided.droppableProps}
ref={provided.innerRef}
className="space-y-2"
>
{localFiles.map((localFile, index) => (
<Draggable
key={localFile.id}
@@ -340,6 +370,7 @@ export const EnvelopeEditorUploadPage = () => {
>
{(provided, snapshot) => (
<div
data-testid={`envelope-item-row-${localFile.id}`}
ref={provided.innerRef}
{...provided.draggableProps}
style={provided.draggableProps.style}
@@ -351,6 +382,7 @@ export const EnvelopeEditorUploadPage = () => {
{uploadConfig?.allowConfigureOrder && (
<div
{...provided.dragHandleProps}
data-testid={`envelope-item-drag-handle-${localFile.id}`}
className="cursor-grab active:cursor-grabbing"
>
<GripVerticalIcon className="h-5 w-5 flex-shrink-0 opacity-40" />
@@ -365,6 +397,7 @@ export const EnvelopeEditorUploadPage = () => {
!uploadConfig?.allowConfigureTitle
}
value={localFile.title}
dataTestId={`envelope-item-title-input-${localFile.id}`}
placeholder={t`Document Title`}
onChange={(title) => {
onEnvelopeItemTitleChange(localFile.envelopeItemId!, title);
@@ -399,7 +432,17 @@ export const EnvelopeEditorUploadPage = () => {
{!localFile.isUploading &&
localFile.envelopeItemId &&
uploadConfig?.allowDelete && (
uploadConfig?.allowDelete &&
(isEmbedded ? (
<Button
variant="ghost"
size="sm"
data-testid={`envelope-item-remove-button-${localFile.id}`}
onClick={() => onFileDelete(localFile.envelopeItemId!)}
>
<X className="h-4 w-4" />
</Button>
) : (
<EnvelopeItemDeleteDialog
canItemBeDeleted={canItemsBeModified}
envelopeId={envelope.id}
@@ -407,12 +450,16 @@ export const EnvelopeEditorUploadPage = () => {
envelopeItemTitle={localFile.title}
onDelete={onFileDelete}
trigger={
<Button variant="ghost" size="sm">
<Button
variant="ghost"
size="sm"
data-testid={`envelope-item-remove-button-${localFile.id}`}
>
<X className="h-4 w-4" />
</Button>
}
/>
)}
))}
</div>
</div>
)}
@@ -0,0 +1,404 @@
import { useLayoutEffect, useMemo, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { useLingui } from '@lingui/react/macro';
import {
DocumentStatus,
EnvelopeType,
ReadStatus,
SendStatus,
SigningStatus,
} from '@prisma/client';
import { CheckCircle2Icon } from 'lucide-react';
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
import type { TDocumentMetaDateFormat } from '@documenso/lib/types/document-meta';
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
import {
type TEmbedCreateEnvelopeAuthoring,
ZEmbedCreateEnvelopeAuthoringSchema,
} from '@documenso/lib/types/envelope-editor';
import type { TEnvelopeFieldAndMeta } from '@documenso/lib/types/field-meta';
import { extractDerivedDocumentMeta } from '@documenso/lib/utils/document';
import { buildEmbeddedFeatures } from '@documenso/lib/utils/embed-config';
import { buildEmbeddedEditorOptions } from '@documenso/lib/utils/embed-config';
import { trpc } from '@documenso/trpc/react';
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { EnvelopeEditor } from '~/components/general/envelope-editor/envelope-editor';
import { EnvelopeEditorRenderProviderWrapper } from '~/components/general/envelope-editor/envelope-editor-renderer-provider-wrapper';
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/envelope.create._index';
export const loader = async ({ request }: Route.LoaderArgs) => {
const url = new URL(request.url);
// We know that the token is present because we're checking it in the parent _layout route
const token = url.searchParams.get('token') || '';
if (!token) {
throw new Response('Invalid token', { status: 404 });
}
// We also know that the token is valid, but we need the userId + teamId
const result = await verifyEmbeddingPresignToken({ token }).catch(() => null);
if (!result) {
throw new Response('Invalid token', { status: 404 });
}
const teamSettings = await getTeamSettings({
userId: result.userId,
teamId: result.teamId,
});
return superLoaderJson({
token,
tokenUserId: result.userId,
tokenTeamId: result.teamId,
teamSettings,
});
};
export default function EmbeddingAuthoringEnvelopeCreatePage() {
const [hasInitialized, setHasInitialized] = useState(false);
const [embedAuthoringOptions, setEmbedAuthoringOptions] =
useState<TEmbedCreateEnvelopeAuthoring | null>(null);
useLayoutEffect(() => {
try {
const hash = window.location.hash.slice(1);
if (hash) {
const result = ZEmbedCreateEnvelopeAuthoringSchema.safeParse(
JSON.parse(decodeURIComponent(atob(hash))),
);
if (result.success) {
setEmbedAuthoringOptions({
...result.data,
features: buildEmbeddedFeatures(result.data.features),
});
}
}
} catch (err) {
console.error('Error parsing embedding params:', err);
}
setHasInitialized(true);
}, []);
if (!hasInitialized) {
return (
<div className="flex min-h-screen items-center justify-center">
<Spinner />
</div>
);
}
if (!embedAuthoringOptions) {
return (
<div className="flex min-h-screen items-center justify-center">
<Trans>Invalid Embedding Parameters</Trans>
</div>
);
}
return <EnvelopeCreatePage embedAuthoringOptions={embedAuthoringOptions} />;
}
type EnvelopeCreatePageProps = {
embedAuthoringOptions: TEmbedCreateEnvelopeAuthoring;
};
const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps) => {
const { token, tokenUserId, tokenTeamId, teamSettings } = useSuperLoaderData<typeof loader>();
const { t } = useLingui();
const { toast } = useToast();
const [isCreatingEnvelope, setIsCreatingEnvelope] = useState(false);
const [createdEnvelope, setCreatedEnvelope] = useState<{ id: string } | null>(null);
const { mutateAsync: createEmbeddingEnvelope } =
trpc.embeddingPresign.createEmbeddingEnvelope.useMutation();
const buildCreateEnvelopeRequest = (
envelope: Omit<TEditorEnvelope, 'id'>,
): { payload: TCreateEnvelopePayload; files: File[] } => {
const sortedItems = [...envelope.envelopeItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
const itemIdToIndex = new Map<string, number>();
sortedItems.forEach((item, index) => {
itemIdToIndex.set(String(item.id), index);
});
const files: File[] = [];
for (const item of sortedItems) {
if (!item.data) {
throw new Error(`Envelope item "${item.title ?? item.id}" has no PDF data`);
}
files.push(
new File(
[item.data],
item.title?.endsWith('.pdf') ? item.title : `${item.title ?? 'document'}.pdf`,
{
type: 'application/pdf',
},
),
);
}
const recipients = envelope.recipients.map((recipient) => {
const recipientFields = envelope.fields.filter((f) => f.recipientId === recipient.id);
const fields = recipientFields.map((field) => {
return {
identifier: itemIdToIndex.get(String(field.envelopeItemId)),
page: field.page,
positionX: Number(field.positionX),
positionY: Number(field.positionY),
width: Number(field.width),
height: Number(field.height),
...({
type: field.type,
fieldMeta: field.fieldMeta ?? undefined,
} as TEnvelopeFieldAndMeta),
};
});
return {
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder ?? undefined,
accessAuth: recipient.authOptions?.accessAuth ?? [],
actionAuth: recipient.authOptions?.actionAuth ?? [],
fields,
};
});
const payload: TCreateEnvelopePayload = {
title: envelope.title,
type: envelope.type,
externalId: envelope.externalId ?? undefined,
visibility: envelope.visibility,
globalAccessAuth: envelope.authOptions?.globalAccessAuth?.length
? envelope.authOptions?.globalAccessAuth
: undefined,
globalActionAuth: envelope.authOptions?.globalActionAuth?.length
? envelope.authOptions?.globalActionAuth
: undefined,
folderId: envelope.folderId ?? undefined,
recipients,
meta: {
subject: envelope.documentMeta.subject ?? undefined,
message: envelope.documentMeta.message ?? undefined,
timezone: envelope.documentMeta.timezone ?? undefined,
dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined,
distributionMethod: envelope.documentMeta.distributionMethod ?? undefined,
signingOrder: envelope.documentMeta.signingOrder ?? undefined,
allowDictateNextSigner: envelope.documentMeta.allowDictateNextSigner ?? undefined,
redirectUrl: envelope.documentMeta.redirectUrl ?? undefined,
language: envelope.documentMeta.language as SupportedLanguageCodes,
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled ?? undefined,
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled ?? undefined,
drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled ?? undefined,
emailId: envelope.documentMeta.emailId ?? undefined,
emailReplyTo: envelope.documentMeta.emailReplyTo ?? undefined,
emailSettings: envelope.documentMeta.emailSettings ?? undefined,
},
};
return { payload, files };
};
const createEmbeddedEnvelope = async (envelopeWithoutId: Omit<TEditorEnvelope, 'id'>) => {
setIsCreatingEnvelope(true);
if (isCreatingEnvelope) {
return;
}
try {
const { payload, files } = buildCreateEnvelopeRequest(envelopeWithoutId);
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
for (const file of files) {
formData.append('files', file);
}
const { id } = await createEmbeddingEnvelope(formData);
// Send a message to the parent window with the document details
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'envelope-created',
envelopeId: id,
externalId: envelopeWithoutId.externalId,
},
'*',
);
}
setCreatedEnvelope({ id });
} catch (err) {
console.error('Failed to create envelope:', err);
toast({
variant: 'destructive',
title: t`Error`,
description: t`Failed to create document. Please try again.`,
});
}
setIsCreatingEnvelope(false);
};
const embeded = useMemo(
() => ({
presignToken: token,
mode: 'create' as const,
onCreate: async (envelope: Omit<TEditorEnvelope, 'id'>) => createEmbeddedEnvelope(envelope),
customBrandingLogo: Boolean(teamSettings.brandingEnabled && teamSettings.brandingLogo),
}),
[token],
);
const editorConfig = useMemo(() => {
return buildEmbeddedEditorOptions(embedAuthoringOptions.features, embeded);
}, [embedAuthoringOptions.features, embeded]);
const initialEnvelope = useMemo((): TEditorEnvelope => {
const defaultDocumentMeta = extractDerivedDocumentMeta(teamSettings, undefined);
const defaultRecipients = teamSettings.defaultRecipients
? ZDefaultRecipientsSchema.parse(teamSettings.defaultRecipients)
: [];
const recipients: TEditorEnvelope['recipients'] = defaultRecipients.map((recipient, index) => ({
id: -(index + 1),
envelopeId: '',
email: recipient.email,
name: recipient.name,
role: recipient.role,
token: '',
readStatus: ReadStatus.NOT_OPENED,
signingStatus: SigningStatus.NOT_SIGNED,
sendStatus: SendStatus.NOT_SENT,
documentDeletedAt: null,
expired: null,
signedAt: null,
authOptions: {
accessAuth: [],
actionAuth: [],
},
signingOrder: index + 1,
rejectionReason: null,
}));
const type = embedAuthoringOptions.type;
return {
id: '',
secondaryId: '',
internalVersion: 2,
type,
status: DocumentStatus.DRAFT,
source: 'DOCUMENT',
visibility: teamSettings.documentVisibility,
templateType: 'PRIVATE',
completedAt: null,
deletedAt: null,
title: type === EnvelopeType.DOCUMENT ? 'Document Title' : 'Template Title',
authOptions: {
globalAccessAuth: [],
globalActionAuth: [],
},
publicTitle: '',
publicDescription: '',
userId: tokenUserId,
teamId: tokenTeamId,
folderId: null,
documentMeta: {
id: '',
...defaultDocumentMeta,
},
recipients,
fields: [],
envelopeItems: [],
directLink: null,
team: {
id: tokenTeamId,
url: '',
},
user: {
id: tokenUserId,
name: '',
email: '',
},
externalId: embedAuthoringOptions?.externalId ?? null,
};
}, []);
return (
<div className="min-w-screen relative min-h-screen">
{isCreatingEnvelope && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
<Spinner />
<p className="mt-2 text-sm text-muted-foreground">
{initialEnvelope.type === EnvelopeType.DOCUMENT ? (
<Trans>Creating Document</Trans>
) : (
<Trans>Creating Template</Trans>
)}
</p>
</div>
)}
{createdEnvelope && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
<div className="mx-auto w-full max-w-md text-center">
<CheckCircle2Icon className="mx-auto h-16 w-16 text-primary" />
<h1 className="mt-6 text-2xl font-bold">
{initialEnvelope.type === EnvelopeType.TEMPLATE ? (
<Trans>Template Created</Trans>
) : (
<Trans>Document Created</Trans>
)}
</h1>
<p className="mt-2 text-muted-foreground">
{initialEnvelope.type === EnvelopeType.TEMPLATE ? (
<Trans>Your template has been created successfully</Trans>
) : (
<Trans>Your document has been created successfully</Trans>
)}
</p>
</div>
</div>
)}
<EnvelopeEditorProvider initialEnvelope={initialEnvelope} editorConfig={editorConfig}>
<EnvelopeEditorRenderProviderWrapper presignedToken={token}>
<EnvelopeEditor />
</EnvelopeEditorRenderProviderWrapper>
</EnvelopeEditorProvider>
</div>
);
};
@@ -0,0 +1,353 @@
import { useLayoutEffect, useMemo, useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { EnvelopeType } from '@prisma/client';
import { CheckCircle2Icon } from 'lucide-react';
import { redirect } from 'react-router';
import { EnvelopeEditorProvider } from '@documenso/lib/client-only/providers/envelope-editor-provider';
import type { SupportedLanguageCodes } from '@documenso/lib/constants/i18n';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
import type { TDocumentMetaDateFormat } from '@documenso/lib/types/document-meta';
import type { TEditorEnvelope } from '@documenso/lib/types/envelope-editor';
import {
type TEmbedEditEnvelopeAuthoring,
ZEmbedEditEnvelopeAuthoringSchema,
} from '@documenso/lib/types/envelope-editor';
import type { TEnvelopeFieldAndMeta } from '@documenso/lib/types/field-meta';
import { buildEmbeddedEditorOptions } from '@documenso/lib/utils/embed-config';
import { trpc } from '@documenso/trpc/react';
import type { TUpdateEmbeddingEnvelopePayload } from '@documenso/trpc/server/embedding-router/update-embedding-envelope.types';
import { Spinner } from '@documenso/ui/primitives/spinner';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { EnvelopeEditor } from '~/components/general/envelope-editor/envelope-editor';
import { EnvelopeEditorRenderProviderWrapper } from '~/components/general/envelope-editor/envelope-editor-renderer-provider-wrapper';
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
import type { Route } from './+types/envelope.edit.$id';
export const loader = async ({ request, params }: Route.LoaderArgs) => {
const url = new URL(request.url);
const { id } = params;
if (!id || !id.startsWith('envelope_')) {
throw redirect(`/embed/v2/authoring/error/not-found`);
}
// We know that the token is present because we're checking it in the parent _layout route
const token = url.searchParams.get('token') || '';
if (!token) {
throw new Response('Invalid token', { status: 404 });
}
// We also know that the token is valid, but we need the userId + teamId
const result = await verifyEmbeddingPresignToken({ token, scope: `envelope:${id}` }).catch(
() => null,
);
if (!result) {
throw new Error('Invalid token');
}
const settings = await getTeamSettings({
userId: result.userId,
teamId: result.teamId,
});
const envelope = await getEnvelopeById({
id: {
type: 'envelopeId',
id,
},
type: null,
userId: result.userId,
teamId: result.teamId,
}).catch(() => null);
if (!envelope) {
throw redirect(`/embed/v2/authoring/error/not-found`);
}
let brandingLogo: string | undefined = undefined;
if (settings.brandingEnabled && settings.brandingLogo) {
brandingLogo = settings.brandingLogo;
}
return superLoaderJson({
token,
envelope,
brandingLogo,
});
};
export default function EmbeddingAuthoringEnvelopeEditPage() {
const [hasInitialized, setHasInitialized] = useState(false);
const [embedAuthoringOptions, setEmbedAuthoringOptions] =
useState<TEmbedEditEnvelopeAuthoring | null>(null);
useLayoutEffect(() => {
try {
const hash = window.location.hash.slice(1);
if (hash) {
const result = ZEmbedEditEnvelopeAuthoringSchema.safeParse(
JSON.parse(decodeURIComponent(atob(hash))),
);
if (result.success) {
setEmbedAuthoringOptions(result.data);
}
}
} catch (err) {
console.error('Error parsing embedding params:', err);
}
setHasInitialized(true);
}, []);
if (!hasInitialized) {
return (
<div className="flex min-h-screen items-center justify-center">
<Spinner />
</div>
);
}
if (!embedAuthoringOptions) {
return (
<div className="flex min-h-screen items-center justify-center">
<Trans>Invalid Embedding Parameters</Trans>
</div>
);
}
return <EnvelopeEditPage embedAuthoringOptions={embedAuthoringOptions} />;
}
type EnvelopeEditPageProps = {
embedAuthoringOptions: TEmbedEditEnvelopeAuthoring;
};
const EnvelopeEditPage = ({ embedAuthoringOptions }: EnvelopeEditPageProps) => {
const { envelope, token, brandingLogo } = useSuperLoaderData<typeof loader>();
const { t } = useLingui();
const { toast } = useToast();
const [isUpdatingEnvelope, setIsUpdatingEnvelope] = useState(false);
const [updatedEnvelope, setUpdatedEnvelope] = useState<{ id: string } | null>(null);
const { mutateAsync: updateEmbeddingEnvelope } =
trpc.embeddingPresign.updateEmbeddingEnvelope.useMutation();
const buildUpdateEnvelopeRequest = (
envelope: TEditorEnvelope,
): { payload: TUpdateEmbeddingEnvelopePayload; files: File[] } => {
const files: File[] = [];
const envelopeItems = envelope.envelopeItems.map((item) => {
// Attach any new envelope item files to the request.
if (item.data) {
files.push(
new File(
[item.data],
item.title?.endsWith('.pdf') ? item.title : `${item.title ?? 'document'}.pdf`,
{
type: 'application/pdf',
},
),
);
}
return {
id: item.id,
title: item.title,
order: item.order,
index: item.data ? files.length - 1 : undefined, // Todo: Embed why not use the ID instead?
};
});
const recipients = envelope.recipients.map((recipient) => {
const recipientFields = envelope.fields.filter((f) => f.recipientId === recipient.id);
const fields = recipientFields.map((field) => ({
id: field.id,
envelopeItemId: field.envelopeItemId,
page: field.page,
positionX: Number(field.positionX),
positionY: Number(field.positionY),
width: Number(field.width),
height: Number(field.height),
...({
type: field.type,
fieldMeta: field.fieldMeta ?? undefined,
} as TEnvelopeFieldAndMeta),
}));
return {
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder ?? undefined,
accessAuth: recipient.authOptions?.accessAuth ?? [],
actionAuth: recipient.authOptions?.actionAuth ?? [],
fields,
};
});
const payload: TUpdateEmbeddingEnvelopePayload = {
envelopeId: envelope.id,
data: {
title: envelope.title,
externalId: envelope.externalId,
visibility: envelope.visibility,
globalAccessAuth: envelope.authOptions?.globalAccessAuth,
globalActionAuth: envelope.authOptions?.globalActionAuth,
folderId: envelope.folderId,
recipients,
envelopeItems,
},
meta: {
subject: envelope.documentMeta.subject ?? undefined,
message: envelope.documentMeta.message ?? undefined,
timezone: envelope.documentMeta.timezone ?? undefined,
distributionMethod: envelope.documentMeta.distributionMethod ?? undefined,
signingOrder: envelope.documentMeta.signingOrder ?? undefined,
allowDictateNextSigner: envelope.documentMeta.allowDictateNextSigner ?? undefined,
redirectUrl: envelope.documentMeta.redirectUrl ?? undefined,
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled ?? undefined,
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled ?? undefined,
drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled ?? undefined,
emailId: envelope.documentMeta.emailId ?? undefined,
emailReplyTo: envelope.documentMeta.emailReplyTo ?? undefined,
emailSettings: envelope.documentMeta.emailSettings ?? undefined,
dateFormat: (envelope.documentMeta.dateFormat as TDocumentMetaDateFormat) ?? undefined,
language: envelope.documentMeta.language as SupportedLanguageCodes,
},
};
return { payload, files };
};
const updateEmbeddedEnvelope = async (envelope: TEditorEnvelope) => {
setIsUpdatingEnvelope(true);
if (isUpdatingEnvelope) {
return;
}
try {
const { payload, files } = buildUpdateEnvelopeRequest(envelope);
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
for (const file of files) {
formData.append('files', file);
}
await updateEmbeddingEnvelope(formData);
// Send a message to the parent window with the document details
if (window.parent !== window) {
window.parent.postMessage(
{
type: 'envelope-updated',
envelopeId: envelope.id,
externalId: envelope.externalId || null,
},
'*',
);
}
// Navigate to the completion page.
setUpdatedEnvelope({ id: envelope.id });
} catch (err) {
console.error('Failed to update envelope:', err);
toast({
variant: 'destructive',
title: t`Error`,
description: t`Failed to update envelope. Please try again.`,
});
}
setIsUpdatingEnvelope(false);
};
const embeded = useMemo(
() => ({
presignToken: token,
mode: 'edit' as const,
onUpdate: async (envelope: TEditorEnvelope) => updateEmbeddedEnvelope(envelope),
brandingLogo,
}),
[token],
);
const editorConfig = useMemo(() => {
return buildEmbeddedEditorOptions(embedAuthoringOptions.features, embeded);
}, [embedAuthoringOptions.features, embeded]);
const initialEnvelope = useMemo(
() => ({
...envelope,
externalId: embedAuthoringOptions?.externalId || envelope.externalId || null,
}),
[envelope, embedAuthoringOptions?.externalId],
);
return (
<div className="min-w-screen relative min-h-screen">
{isUpdatingEnvelope && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
<Spinner />
<p className="mt-2 text-sm text-muted-foreground">
{envelope.type === EnvelopeType.DOCUMENT ? (
<Trans>Updating Document</Trans>
) : (
<Trans>Updating Template</Trans>
)}
</p>
</div>
)}
{updatedEnvelope && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-background">
<div className="mx-auto w-full max-w-md text-center">
<CheckCircle2Icon className="mx-auto h-16 w-16 text-primary" />
<h1 className="mt-6 text-2xl font-bold">
{envelope.type === EnvelopeType.TEMPLATE ? (
<Trans>Template Updated</Trans>
) : (
<Trans>Document Updated</Trans>
)}
</h1>
<p className="mt-2 text-muted-foreground">
{envelope.type === EnvelopeType.TEMPLATE ? (
<Trans>Your template has been updated successfully</Trans>
) : (
<Trans>Your document has been updated successfully</Trans>
)}
</p>
</div>
</div>
)}
<EnvelopeEditorProvider initialEnvelope={initialEnvelope} editorConfig={editorConfig}>
<EnvelopeEditorRenderProviderWrapper presignedToken={token}>
<EnvelopeEditor />
</EnvelopeEditorRenderProviderWrapper>
</EnvelopeEditorProvider>
</div>
);
};