Compare commits

..

2 Commits

Author SHA1 Message Date
044b267016 fix: handle loading files in embedded authoring update flows 2025-11-19 23:22:33 +11:00
fa1680aaf1 v2.0.13 2025-11-18 16:59:02 +11:00
13 changed files with 111 additions and 95 deletions

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentData, FieldType } from '@prisma/client';
import type { EnvelopeItem, FieldType } from '@prisma/client';
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
import { base64 } from '@scure/base';
import { ChevronsUpDown } from 'lucide-react';
@ -40,7 +40,8 @@ const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type ConfigureFieldsViewProps = {
configData: TConfigureEmbedFormSchema;
documentData?: DocumentData;
presignToken?: string | undefined;
envelopeItem?: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
defaultValues?: Partial<TConfigureFieldsFormSchema>;
onBack?: (data: TConfigureFieldsFormSchema) => void;
onSubmit: (data: TConfigureFieldsFormSchema) => void;
@ -48,7 +49,8 @@ export type ConfigureFieldsViewProps = {
export const ConfigureFieldsView = ({
configData,
documentData,
presignToken,
envelopeItem,
defaultValues,
onBack,
onSubmit,
@ -82,12 +84,12 @@ export const ConfigureFieldsView = ({
}, []);
const normalizedDocumentData = useMemo(() => {
if (documentData) {
return documentData.data;
if (envelopeItem) {
return undefined;
}
if (!configData.documentData) {
return null;
return undefined;
}
return base64.encode(configData.documentData.data);
@ -534,56 +536,55 @@ export const ConfigureFieldsView = ({
)}
<Form {...form}>
{normalizedDocumentData && (
<div>
<PDFViewer
overrideData={normalizedDocumentData}
envelopeItem={{
id: '',
envelopeId: '',
}}
token={undefined}
version="signed"
/>
{normalizedDocumentData ||
(envelopeItem && (
<div>
<PDFViewer
presignToken={presignToken}
overrideData={normalizedDocumentData}
envelopeItem={envelopeItem}
token={undefined}
version="signed"
/>
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
{localFields.map((field, index) => {
const recipientIndex = recipients.findIndex(
(r) => r.id === field.recipientId,
);
<ElementVisible
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
>
{localFields.map((field, index) => {
const recipientIndex = recipients.findIndex(
(r) => r.id === field.recipientId,
);
return (
<FieldItem
key={field.formId}
field={field}
minHeight={MIN_HEIGHT_PX}
minWidth={MIN_WIDTH_PX}
defaultHeight={DEFAULT_HEIGHT_PX}
defaultWidth={DEFAULT_WIDTH_PX}
onResize={(node) => onFieldResize(node, index)}
onMove={(node) => onFieldMove(node, index)}
onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)}
onAdvancedSettings={() => {
setCurrentField(field);
setShowAdvancedSettings(true);
}}
recipientIndex={recipientIndex}
active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)}
onFieldDeactivate={() => setActiveFieldId(null)}
disabled={selectedRecipient?.id !== field.recipientId}
/>
);
})}
</ElementVisible>
</div>
)}
return (
<FieldItem
key={field.formId}
field={field}
minHeight={MIN_HEIGHT_PX}
minWidth={MIN_WIDTH_PX}
defaultHeight={DEFAULT_HEIGHT_PX}
defaultWidth={DEFAULT_WIDTH_PX}
onResize={(node) => onFieldResize(node, index)}
onMove={(node) => onFieldMove(node, index)}
onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)}
onAdvancedSettings={() => {
setCurrentField(field);
setShowAdvancedSettings(true);
}}
recipientIndex={recipientIndex}
active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)}
onFieldDeactivate={() => setActiveFieldId(null)}
disabled={selectedRecipient?.id !== field.recipientId}
/>
);
})}
</ElementVisible>
</div>
))}
</Form>
</div>
</div>

View File

@ -84,13 +84,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
const canSignUp = !isExistingUser && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true';
const redirectToFolder =
user && document.userId === user.id && document.folderId && document.team?.url;
const homePath = redirectToFolder
? `/t/${document.team.url}/documents/f/${document.folderId}`
: '/';
return {
isDocumentAccessValid: true,
canSignUp,
@ -99,7 +92,6 @@ export async function loader({ params, request }: Route.LoaderArgs) {
signatures,
document,
recipient,
homePath,
};
}
@ -117,7 +109,6 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
document,
recipient,
recipientEmail,
homePath,
} = loaderData;
if (!isDocumentAccessValid) {
@ -211,12 +202,8 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
</p>
))}
<div className="mt-8 flex w-full max-w-xs flex-col items-stretch gap-4 md:w-auto md:max-w-none md:flex-row md:items-center">
<DocumentShareButton
documentId={document.id}
token={recipient.token}
className="w-full max-w-none md:flex-1"
/>
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
<DocumentShareButton documentId={document.id} token={recipient.token} />
{isDocumentCompleted(document.status) && (
<EnvelopeDownloadDialog
@ -225,21 +212,13 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
envelopeItems={document.envelopeItems}
token={recipient?.token}
trigger={
<Button type="button" variant="outline" className="flex-1 md:flex-initial">
<Button type="button" variant="outline" className="flex-1">
<DownloadIcon className="mr-2 h-5 w-5" />
<Trans>Download</Trans>
</Button>
}
/>
)}
{user && (
<Button asChild>
<Link to={homePath}>
<Trans>Go Back Home</Trans>
</Link>
</Button>
)}
</div>
</div>
@ -259,6 +238,12 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
<ClaimAccount defaultName={recipientName} defaultEmail={recipient.email} />
</div>
)}
{user && (
<Link to="/" className="text-documenso-700 hover:text-documenso-600 mt-2">
<Trans>Go Back Home</Trans>
</Link>
)}
</div>
</div>

View File

@ -75,6 +75,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
}));
return {
token,
document: {
...document,
fields,
@ -86,7 +87,7 @@ export default function EmbeddingAuthoringDocumentEditPage() {
const { _ } = useLingui();
const { toast } = useToast();
const { document } = useLoaderData<typeof loader>();
const { document, token } = useLoaderData<typeof loader>();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
@ -321,7 +322,8 @@ export default function EmbeddingAuthoringDocumentEditPage() {
<ConfigureFieldsView
configData={configuration!}
documentData={document.documentData}
presignToken={token}
envelopeItem={document.envelopeItems[0]}
defaultValues={fields ?? undefined}
onBack={canGoBack ? handleBackToConfig : undefined}
onSubmit={handleConfigureFieldsSubmit}

View File

@ -75,6 +75,7 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => {
}));
return {
token,
template: {
...template,
fields,
@ -86,7 +87,7 @@ export default function EmbeddingAuthoringTemplateEditPage() {
const { _ } = useLingui();
const { toast } = useToast();
const { template } = useLoaderData<typeof loader>();
const { template, token } = useLoaderData<typeof loader>();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
@ -321,7 +322,8 @@ export default function EmbeddingAuthoringTemplateEditPage() {
<ConfigureFieldsView
configData={configuration!}
documentData={template.templateDocumentData}
presignToken={token}
envelopeItem={template.envelopeItems[0]}
defaultValues={fields ?? undefined}
onBack={canGoBack ? handleBackToConfig : undefined}
onSubmit={handleConfigureFieldsSubmit}

View File

@ -106,5 +106,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "2.0.12"
"version": "2.0.13"
}

View File

@ -5,6 +5,7 @@ import { Hono } from 'hono';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
@ -16,6 +17,7 @@ import {
type TGetPresignedPostUrlResponse,
ZGetEnvelopeItemFileDownloadRequestParamsSchema,
ZGetEnvelopeItemFileRequestParamsSchema,
ZGetEnvelopeItemFileRequestQuerySchema,
ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema,
ZGetEnvelopeItemFileTokenRequestParamsSchema,
ZGetPresignedPostUrlRequestSchema,
@ -68,12 +70,24 @@ export const filesRoute = new Hono<HonoEnv>()
.get(
'/envelope/:envelopeId/envelopeItem/:envelopeItemId',
sValidator('param', ZGetEnvelopeItemFileRequestParamsSchema),
sValidator('query', ZGetEnvelopeItemFileRequestQuerySchema),
async (c) => {
const { envelopeId, envelopeItemId } = c.req.valid('param');
const { token } = c.req.query();
const session = await getOptionalSession(c);
if (!session.user) {
let userId = session.user?.id;
if (token) {
const presignToken = await verifyEmbeddingPresignToken({
token,
}).catch(() => undefined);
userId = presignToken?.userId;
}
if (!userId) {
return c.json({ error: 'Unauthorized' }, 401);
}
@ -104,7 +118,7 @@ export const filesRoute = new Hono<HonoEnv>()
}
const team = await getTeamById({
userId: session.user.id,
userId: userId,
teamId: envelope.teamId,
}).catch((error) => {
console.error(error);

View File

@ -36,6 +36,14 @@ export type TGetEnvelopeItemFileRequestParams = z.infer<
typeof ZGetEnvelopeItemFileRequestParamsSchema
>;
export const ZGetEnvelopeItemFileRequestQuerySchema = z.object({
token: z.string().optional(),
});
export type TGetEnvelopeItemFileRequestQuery = z.infer<
typeof ZGetEnvelopeItemFileRequestQuerySchema
>;
export const ZGetEnvelopeItemFileTokenRequestParamsSchema = z.object({
token: z.string().min(1),
envelopeItemId: z.string().min(1),

6
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "2.0.12",
"version": "2.0.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "2.0.12",
"version": "2.0.13",
"workspaces": [
"apps/*",
"packages/*"
@ -101,7 +101,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "2.0.12",
"version": "2.0.13",
"dependencies": {
"@cantoo/pdf-lib": "^2.5.2",
"@documenso/api": "*",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "2.0.12",
"version": "2.0.13",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",

View File

@ -103,7 +103,6 @@ export const getDocumentAndSenderByToken = async ({
select: {
name: true,
teamEmail: true,
url: true,
teamGlobalSettings: {
select: {
brandingEnabled: true,

View File

@ -8,15 +8,17 @@ export type EnvelopeItemPdfUrlOptions =
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
version: 'original' | 'signed';
presignToken?: undefined;
}
| {
type: 'view';
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
presignToken?: string | undefined;
};
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
const { envelopeItem, token, type } = options;
const { envelopeItem, token, type, presignToken } = options;
const { id, envelopeId } = envelopeItem;
@ -24,11 +26,11 @@ export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
const version = options.version;
return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}${presignToken ? `?presignToken=${presignToken}` : ''}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
}
return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}`
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}${presignToken ? `?presignToken=${presignToken}` : ''}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
};

View File

@ -127,11 +127,11 @@ export const DocumentShareButton = ({
<Button
variant="outline"
disabled={!token || !documentId}
className={cn('h-11 w-full max-w-lg flex-1', className)}
className={cn('flex-1 text-[11px]', className)}
loading={isLoading}
>
{!isLoading && <Sparkles className="mr-2 h-5 w-5" />}
<Trans>Share</Trans>
<Trans>Share Signature Card</Trans>
</Button>
)}
</DialogTrigger>

View File

@ -56,6 +56,7 @@ export type PDFViewerProps = {
className?: string;
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
presignToken?: string | undefined;
version: 'original' | 'signed';
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
onPageClick?: OnPDFViewerPageClick;
@ -67,6 +68,7 @@ export const PDFViewer = ({
className,
envelopeItem,
token,
presignToken,
version,
onDocumentLoad,
onPageClick,
@ -166,6 +168,7 @@ export const PDFViewer = ({
type: 'view',
envelopeItem: envelopeItem,
token,
presignToken,
});
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());