mirror of
https://github.com/documenso/documenso.git
synced 2026-08-22 14:22:21 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6ad94a58f |
@@ -29,9 +29,9 @@ type EnvelopeDownloadDialogProps = {
|
|||||||
* button is hidden for them.
|
* button is hidden for them.
|
||||||
*
|
*
|
||||||
* Optional: omit it on call sites where the status can never be PENDING (DRAFT,
|
* Optional: omit it on call sites where the status can never be PENDING (DRAFT,
|
||||||
* COMPLETED, REJECTED) or when a recipient token is set, since the Partial button
|
* COMPLETED, REJECTED) or that only render v2 envelopes, such as the v2 signing
|
||||||
* is also gated on those. Pass it from team-side call sites that can render the
|
* page. Pass it from call sites that can render the dialog for a PENDING
|
||||||
* dialog for a PENDING envelope.
|
* envelope of either version.
|
||||||
*/
|
*/
|
||||||
isLegacy?: boolean;
|
isLegacy?: boolean;
|
||||||
envelopeItems?: EnvelopeItemToDownload[];
|
envelopeItems?: EnvelopeItemToDownload[];
|
||||||
@@ -67,12 +67,10 @@ export const EnvelopeDownloadDialog = ({
|
|||||||
|
|
||||||
// The dialog shows the original document alongside one of:
|
// The dialog shows the original document alongside one of:
|
||||||
// - "Signed" (when the envelope is COMPLETED)
|
// - "Signed" (when the envelope is COMPLETED)
|
||||||
// - "Partial" (when the envelope is PENDING, not legacy, and we are on the
|
// - "Partial" (when the envelope is PENDING and not legacy; legacy envelopes
|
||||||
// team/owner side; recipients are intentionally not offered this since the
|
// use a different rendering pipeline that the partial-download helper does
|
||||||
// partial PDF carries no PKI signature and would create a leak vector for
|
// not implement)
|
||||||
// half-executed contracts; legacy envelopes use a different rendering
|
// - nothing (DRAFT, REJECTED, or legacy PENDING)
|
||||||
// pipeline that the partial-download helper does not implement)
|
|
||||||
// - nothing (DRAFT, REJECTED, PENDING with recipient token, or legacy PENDING)
|
|
||||||
const secondaryDownload = useMemo<{ version: 'signed' | 'pending'; label: string } | null>(() => {
|
const secondaryDownload = useMemo<{ version: 'signed' | 'pending'; label: string } | null>(() => {
|
||||||
if (envelopeStatus === DocumentStatus.COMPLETED) {
|
if (envelopeStatus === DocumentStatus.COMPLETED) {
|
||||||
return {
|
return {
|
||||||
@@ -81,7 +79,7 @@ export const EnvelopeDownloadDialog = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (envelopeStatus === DocumentStatus.PENDING && !token && !isLegacy) {
|
if (envelopeStatus === DocumentStatus.PENDING && !isLegacy) {
|
||||||
return {
|
return {
|
||||||
version: 'pending',
|
version: 'pending',
|
||||||
label: t({ message: 'Partial', context: 'Partially signed document (adjective)' }),
|
label: t({ message: 'Partial', context: 'Partially signed document (adjective)' }),
|
||||||
@@ -89,7 +87,7 @@ export const EnvelopeDownloadDialog = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [envelopeStatus, isLegacy, token, t]);
|
}, [envelopeStatus, isLegacy, t]);
|
||||||
|
|
||||||
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getManyByToken.useQuery(
|
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } = trpc.envelope.item.getManyByToken.useQuery(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -148,6 +148,13 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The envelope may complete or get rejected while this page polls, so derive
|
||||||
|
// the download dialog status from the live signing status.
|
||||||
|
const envelopeStatus = match(signingStatus)
|
||||||
|
.with('COMPLETED', () => DocumentStatus.COMPLETED)
|
||||||
|
.with('REJECTED', () => DocumentStatus.REJECTED)
|
||||||
|
.otherwise(() => document.status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RecipientBranding branding={branding} cspNonce={cspNonce} />
|
<RecipientBranding branding={branding} cspNonce={cspNonce} />
|
||||||
@@ -255,10 +262,11 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
className="w-full max-w-none md:flex-1"
|
className="w-full max-w-none md:flex-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isDocumentCompleted(document) && (
|
{(isDocumentCompleted(envelopeStatus) || envelopeStatus === DocumentStatus.PENDING) && (
|
||||||
<EnvelopeDownloadDialog
|
<EnvelopeDownloadDialog
|
||||||
envelopeId={document.envelopeId}
|
envelopeId={document.envelopeId}
|
||||||
envelopeStatus={document.status}
|
envelopeStatus={envelopeStatus}
|
||||||
|
isLegacy={document.internalVersion === 1}
|
||||||
envelopeItems={document.envelopeItems}
|
envelopeItems={document.envelopeItems}
|
||||||
token={recipient?.token}
|
token={recipient?.token}
|
||||||
trigger={
|
trigger={
|
||||||
|
|||||||
@@ -83,16 +83,7 @@ export const downloadRoute = new Hono<HonoEnv>()
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
envelope: {
|
envelope: true,
|
||||||
include: {
|
|
||||||
recipients: {
|
|
||||||
select: {
|
|
||||||
role: true,
|
|
||||||
signingStatus: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
documentData: true,
|
documentData: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import {
|
|||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
type EnvelopeType,
|
type EnvelopeType,
|
||||||
EnvelopeType as EnvelopeTypeEnum,
|
EnvelopeType as EnvelopeTypeEnum,
|
||||||
type RecipientRole,
|
SigningStatus,
|
||||||
type SigningStatus,
|
|
||||||
type TemplateType,
|
type TemplateType,
|
||||||
TemplateType as TemplateTypeEnum,
|
TemplateType as TemplateTypeEnum,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
@@ -55,17 +54,13 @@ type EnvelopeForPendingDownload = {
|
|||||||
id: string;
|
id: string;
|
||||||
status: DocumentStatus;
|
status: DocumentStatus;
|
||||||
internalVersion: number;
|
internalVersion: number;
|
||||||
recipients: Array<{
|
|
||||||
role: RecipientRole;
|
|
||||||
signingStatus: SigningStatus;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options shape varies by `version`:
|
* Options shape varies by `version`:
|
||||||
* - `signed` / `original`: serves stored bytes; only needs envelope `status` for cache headers.
|
* - `signed` / `original`: serves stored bytes; only needs envelope `status` for cache headers.
|
||||||
* - `pending`: generates a fresh PDF with currently-inserted fields burned in; needs the
|
* - `pending`: generates a fresh PDF with currently-inserted fields burned in; needs the
|
||||||
* full envelope (id, status, internalVersion, recipients) plus envelopeItemId to query fields.
|
* envelope (id, status, internalVersion) plus envelopeItemId to query fields.
|
||||||
*/
|
*/
|
||||||
type HandleEnvelopeItemFileRequestOptions = {
|
type HandleEnvelopeItemFileRequestOptions = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -81,6 +76,13 @@ type HandleEnvelopeItemFileRequestOptions = {
|
|||||||
version: 'pending';
|
version: 'pending';
|
||||||
envelopeItemId: string;
|
envelopeItemId: string;
|
||||||
envelope: EnvelopeForPendingDownload;
|
envelope: EnvelopeForPendingDownload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When set, only fields from recipients who have signed, plus the fields of
|
||||||
|
* the recipient owning this token, are burned in. Keeps recipient downloads
|
||||||
|
* in parity with what the signing page shows them.
|
||||||
|
*/
|
||||||
|
recipientToken?: string;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -165,6 +167,7 @@ const handlePendingFileRequest = async ({
|
|||||||
envelopeItemId,
|
envelopeItemId,
|
||||||
envelope,
|
envelope,
|
||||||
documentData,
|
documentData,
|
||||||
|
recipientToken,
|
||||||
context: c,
|
context: c,
|
||||||
}: PendingFileRequestOptions) => {
|
}: PendingFileRequestOptions) => {
|
||||||
if (envelope.status !== DocumentStatus.PENDING) {
|
if (envelope.status !== DocumentStatus.PENDING) {
|
||||||
@@ -191,6 +194,13 @@ const handlePendingFileRequest = async ({
|
|||||||
where: {
|
where: {
|
||||||
envelopeItemId,
|
envelopeItemId,
|
||||||
inserted: true,
|
inserted: true,
|
||||||
|
...(recipientToken
|
||||||
|
? {
|
||||||
|
recipient: {
|
||||||
|
OR: [{ signingStatus: SigningStatus.SIGNED }, { token: recipientToken }],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
signature: true,
|
signature: true,
|
||||||
|
|||||||
@@ -160,12 +160,6 @@ export const filesRoute = new Hono<HonoEnv>()
|
|||||||
documentData: true,
|
documentData: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
recipients: {
|
|
||||||
select: {
|
|
||||||
role: true,
|
|
||||||
signingStatus: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -289,52 +283,80 @@ export const filesRoute = new Hono<HonoEnv>()
|
|||||||
'/token/:token/envelopeItem/:envelopeItemId/download/:version?',
|
'/token/:token/envelopeItem/:envelopeItemId/download/:version?',
|
||||||
sValidator('param', ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema),
|
sValidator('param', ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { token, envelopeItemId, version } = c.req.valid('param');
|
const logger = c.get('logger');
|
||||||
|
|
||||||
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
try {
|
||||||
id: envelopeItemId,
|
const { token, envelopeItemId, version } = c.req.valid('param');
|
||||||
envelope: {
|
|
||||||
recipients: {
|
|
||||||
some: {
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (token.startsWith('qr_')) {
|
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
||||||
envelopeWhereQuery = {
|
|
||||||
id: envelopeItemId,
|
id: envelopeItemId,
|
||||||
envelope: {
|
envelope: {
|
||||||
qrToken: token,
|
recipients: {
|
||||||
|
some: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (token.startsWith('qr_')) {
|
||||||
|
envelopeWhereQuery = {
|
||||||
|
id: envelopeItemId,
|
||||||
|
envelope: {
|
||||||
|
qrToken: token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const envelopeItem = await prisma.envelopeItem.findUnique({
|
||||||
|
where: envelopeWhereQuery,
|
||||||
|
include: {
|
||||||
|
envelope: true,
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envelopeItem) {
|
||||||
|
return c.json({ error: 'Envelope item not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envelopeItem.documentData) {
|
||||||
|
return c.json({ error: 'Document data not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseOptions = {
|
||||||
|
title: envelopeItem.title,
|
||||||
|
documentData: envelopeItem.documentData,
|
||||||
|
isDownload: true,
|
||||||
|
context: c,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (version === 'pending') {
|
||||||
|
return await handleEnvelopeItemFileRequest({
|
||||||
|
...baseOptions,
|
||||||
|
version,
|
||||||
|
envelopeItemId: envelopeItem.id,
|
||||||
|
envelope: envelopeItem.envelope,
|
||||||
|
recipientToken: token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await handleEnvelopeItemFileRequest({
|
||||||
|
...baseOptions,
|
||||||
|
version,
|
||||||
|
status: envelopeItem.envelope.status,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
const { status, body } = AppError.toRestAPIError(error);
|
||||||
|
|
||||||
|
return c.json({ error: body.message, code: error.code }, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envelopeItem = await prisma.envelopeItem.findUnique({
|
|
||||||
where: envelopeWhereQuery,
|
|
||||||
include: {
|
|
||||||
envelope: true,
|
|
||||||
documentData: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!envelopeItem) {
|
|
||||||
return c.json({ error: 'Envelope item not found' }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!envelopeItem.documentData) {
|
|
||||||
return c.json({ error: 'Document data not found' }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await handleEnvelopeItemFileRequest({
|
|
||||||
title: envelopeItem.title,
|
|
||||||
status: envelopeItem.envelope.status,
|
|
||||||
documentData: envelopeItem.documentData,
|
|
||||||
version,
|
|
||||||
isDownload: true,
|
|
||||||
context: c,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export type TGetEnvelopeItemFileDownloadRequestParams = z.infer<typeof ZGetEnvel
|
|||||||
export const ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema = z.object({
|
export const ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema = z.object({
|
||||||
token: z.string().min(1),
|
token: z.string().min(1),
|
||||||
envelopeItemId: z.string().min(1),
|
envelopeItemId: z.string().min(1),
|
||||||
version: z.enum(['signed', 'original']).default('signed'),
|
version: z.enum(['signed', 'original', 'pending']).default('signed'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TGetEnvelopeItemFileTokenDownloadRequestParams = z.infer<
|
export type TGetEnvelopeItemFileTokenDownloadRequestParams = z.infer<
|
||||||
|
|||||||
@@ -241,4 +241,123 @@ test.describe('API V2 partial signed PDF downloads', () => {
|
|||||||
expect(legacyResponse.status()).toBe(400);
|
expect(legacyResponse.status()).toBe(400);
|
||||||
expect(legacyError.code).toBe('ENVELOPE_LEGACY');
|
expect(legacyError.code).toBe('ENVELOPE_LEGACY');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('allows recipients to download the partial PDF via their token', async ({ request }) => {
|
||||||
|
const { envelope, distributeResult } = await apiSeedPendingDocument(request, {
|
||||||
|
recipients: [
|
||||||
|
{ email: 'partial-token-1@test.documenso.com', name: 'Partial Token 1' },
|
||||||
|
{ email: 'partial-token-2@test.documenso.com', name: 'Partial Token 2' },
|
||||||
|
],
|
||||||
|
fieldsPerRecipient: [
|
||||||
|
[{ type: FieldType.SIGNATURE, page: 1, positionX: 5, positionY: 5, width: 15, height: 5 }],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
page: 1,
|
||||||
|
positionX: 5,
|
||||||
|
positionY: 15,
|
||||||
|
width: 15,
|
||||||
|
height: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [recipientOne, recipientTwo] = distributeResult.recipients;
|
||||||
|
const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||||
|
const envelopeItem = envelope.envelopeItems[0];
|
||||||
|
const recipientOneField = envelope.fields.find((field) => field.recipientId === recipientOne.id);
|
||||||
|
|
||||||
|
if (!recipientOneField) {
|
||||||
|
throw new Error('Expected signature field not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenDownloadUrl = (token: string) =>
|
||||||
|
`${WEBAPP_BASE_URL}/api/files/token/${token}/envelopeItem/${envelopeItem.id}/download/pending`;
|
||||||
|
|
||||||
|
// Recipient one inserts their field without completing the document.
|
||||||
|
await trpcMutation(request, 'envelope.field.sign', {
|
||||||
|
token: recipientOne.token,
|
||||||
|
fieldId: recipientOneField.id,
|
||||||
|
fieldValue: {
|
||||||
|
type: FieldType.SIGNATURE,
|
||||||
|
value: 'Signature',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipientOneResponse = await request.get(tokenDownloadUrl(recipientOne.token));
|
||||||
|
|
||||||
|
expect(recipientOneResponse.status()).toBe(200);
|
||||||
|
expect(recipientOneResponse.headers()['content-type']).toContain('application/pdf');
|
||||||
|
expect(recipientOneResponse.headers()['cache-control']).toBe('no-store, private');
|
||||||
|
expect(recipientOneResponse.headers()['content-disposition']).toContain('_pending.pdf');
|
||||||
|
await getPdfBytes(recipientOneResponse);
|
||||||
|
|
||||||
|
// Recipient two must not see recipient one's in-progress field. The ETag is
|
||||||
|
// derived from the included fields, so the two downloads must differ.
|
||||||
|
const recipientTwoResponse = await request.get(tokenDownloadUrl(recipientTwo.token));
|
||||||
|
|
||||||
|
expect(recipientTwoResponse.status()).toBe(200);
|
||||||
|
await getPdfBytes(recipientTwoResponse);
|
||||||
|
|
||||||
|
expect(recipientOneResponse.headers().etag).not.toBe(recipientTwoResponse.headers().etag);
|
||||||
|
|
||||||
|
// Once recipient one completes, their field becomes visible to recipient two.
|
||||||
|
await trpcMutation(request, 'recipient.completeDocumentWithToken', {
|
||||||
|
token: recipientOne.token,
|
||||||
|
documentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const dbRecipient = await prisma.recipient.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: recipientOne.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dbRecipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
|
const afterCompletionResponse = await request.get(tokenDownloadUrl(recipientTwo.token));
|
||||||
|
|
||||||
|
expect(afterCompletionResponse.status()).toBe(200);
|
||||||
|
expect(afterCompletionResponse.headers().etag).toBe(recipientOneResponse.headers().etag);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a recipient token pending download once the envelope is completed', async ({ request }) => {
|
||||||
|
const { envelope, distributeResult } = await apiSeedPendingDocument(request);
|
||||||
|
|
||||||
|
const [recipient] = distributeResult.recipients;
|
||||||
|
const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
||||||
|
const recipientField = envelope.fields.find((field) => field.recipientId === recipient.id);
|
||||||
|
|
||||||
|
if (!recipientField) {
|
||||||
|
throw new Error('Expected signature field not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await signAndCompleteRecipient({
|
||||||
|
request,
|
||||||
|
token: recipient.token,
|
||||||
|
documentId,
|
||||||
|
fieldId: recipientField.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
const dbEnvelope = await prisma.envelope.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: envelope.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(dbEnvelope.status).toBe(DocumentStatus.COMPLETED);
|
||||||
|
}).toPass({ timeout: 15_000 });
|
||||||
|
|
||||||
|
const completedResponse = await request.get(
|
||||||
|
`${WEBAPP_BASE_URL}/api/files/token/${recipient.token}/envelopeItem/${envelope.envelopeItems[0].id}/download/pending`,
|
||||||
|
);
|
||||||
|
const completedError = await completedResponse.json();
|
||||||
|
|
||||||
|
expect(completedResponse.status()).toBe(400);
|
||||||
|
expect(completedError.code).toBe('ENVELOPE_COMPLETED');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ type DownloadPDFProps = {
|
|||||||
* 'signed': Downloads the signed version (default).
|
* 'signed': Downloads the signed version (default).
|
||||||
* 'original': Downloads the original version.
|
* 'original': Downloads the original version.
|
||||||
* 'pending': Downloads the original document with currently-inserted fields burned in.
|
* 'pending': Downloads the original document with currently-inserted fields burned in.
|
||||||
* Only valid while the envelope is in PENDING status. Not supported via
|
* Only valid while the envelope is in PENDING status.
|
||||||
* recipient token.
|
|
||||||
*/
|
*/
|
||||||
version?: DocumentVersion;
|
version?: DocumentVersion;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { EnvelopeItem } from '@prisma/client';
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `pending` is only supported when there is no recipient token (team/owner-side downloads
|
* `pending` downloads a PDF with the currently-inserted fields burned in. Supported
|
||||||
* via the session-authed file route). The recipient-token route does not accept `pending`.
|
* for both session-authed and recipient-token downloads while the envelope is PENDING.
|
||||||
*/
|
*/
|
||||||
export type EnvelopeItemPdfUrlOptions =
|
export type EnvelopeItemPdfUrlOptions =
|
||||||
| {
|
| {
|
||||||
|
|||||||
Reference in New Issue
Block a user