diff --git a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx
index 9e2b1e4aa..ad7359c3f 100644
--- a/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx
+++ b/apps/remix/app/components/general/envelope-editor/envelope-generic-page-renderer.tsx
@@ -25,12 +25,17 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender
envelopeStatus,
currentEnvelopeItem,
fields,
+ signatures,
recipients,
getRecipientColorKey,
setRenderError,
overrideSettings,
} = useCurrentEnvelopeRender();
+ const signaturesByFieldId = useMemo(() => {
+ return new Map(signatures.map((signature) => [signature.fieldId, signature]));
+ }, [signatures]);
+
const { stage, pageLayer, konvaContainer, unscaledViewport } = usePageRenderer(
({ stage, pageLayer }) => {
createPageCanvas(stage, pageLayer);
@@ -80,6 +85,16 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender
const fieldTranslations = getClientSideFieldTranslations(i18n);
+ // Look up an inserted signature for this field. If we don't have one (e.g.
+ // the signatures haven't been loaded, or the field hasn't been signed yet)
+ // fall back to a placeholder so the field still renders something.
+ const insertedSignature = signaturesByFieldId.get(field.id);
+
+ const signature = insertedSignature ?? {
+ signatureImageAsBase64: '',
+ typedSignature: fieldTranslations.SIGNATURE,
+ };
+
renderField({
scale,
pageLayer: pageLayer.current,
@@ -91,10 +106,7 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender
positionX: Number(field.positionX),
positionY: Number(field.positionY),
fieldMeta: field.fieldMeta,
- signature: {
- signatureImageAsBase64: '',
- typedSignature: fieldTranslations.SIGNATURE,
- },
+ signature,
},
translations: fieldTranslations,
pageWidth: unscaledViewport.width,
@@ -150,7 +162,7 @@ export const EnvelopeGenericPageRenderer = ({ pageData }: { pageData: PageRender
});
pageLayer.current.batchDraw();
- }, [localPageFields]);
+ }, [localPageFields, signaturesByFieldId]);
if (!currentEnvelopeItem) {
return null;
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
index aac61078f..534cc1ab2 100644
--- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx
@@ -62,6 +62,17 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
},
);
+ const { data: fieldSignatures } = trpc.envelope.field.getSignatures.useQuery(
+ {
+ envelopeId: params.id,
+ },
+ {
+ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
+ enabled:
+ envelope && envelope.internalVersion === 2 && envelope.status === DocumentStatus.PENDING,
+ },
+ );
+
if (isLoadingEnvelope) {
return (
@@ -159,6 +170,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
envelopeItems={envelope.envelopeItems}
token={undefined}
fields={envelope.fields}
+ signatures={fieldSignatures}
recipients={envelope.recipients}
overrideSettings={{
showRecipientSigningStatus: true,
diff --git a/packages/lib/client-only/providers/envelope-render-provider.tsx b/packages/lib/client-only/providers/envelope-render-provider.tsx
index c66b0a38f..101488315 100644
--- a/packages/lib/client-only/providers/envelope-render-provider.tsx
+++ b/packages/lib/client-only/providers/envelope-render-provider.tsx
@@ -11,6 +11,18 @@ import { getRecipientColor } from '@documenso/ui/lib/recipient-colors';
import type { TEnvelope } from '../../types/envelope';
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
+/**
+ * The signature data for an inserted signature field.
+ *
+ * Loaded separately from the envelope to avoid bloating the envelope.get response
+ * with potentially large base64 image payloads.
+ */
+export type EnvelopeRenderFieldSignature = {
+ fieldId: number;
+ signatureImageAsBase64: string | null;
+ typedSignature: string | null;
+};
+
export type PageRenderData = {
scale: number;
pageIndex: number;
@@ -50,6 +62,7 @@ type EnvelopeRenderProviderValue = {
currentEnvelopeItem: EnvelopeRenderItem | null;
setCurrentEnvelopeItem: (envelopeItemId: string) => void;
fields: Field[];
+ signatures: EnvelopeRenderFieldSignature[];
recipients: Pick[];
getRecipientColorKey: (recipientId: number) => TRecipientColor;
@@ -89,6 +102,15 @@ interface EnvelopeRenderProviderProps {
*/
fields?: Field[];
+ /**
+ * Optional inserted signature data for signature fields.
+ *
+ * Fetched separately from the envelope to keep the envelope response lean.
+ * If a signature field has no entry here, the renderer will fall back to
+ * showing the field type placeholder.
+ */
+ signatures?: EnvelopeRenderFieldSignature[];
+
/**
* Optional recipient used to determine the color of the fields and hover
* previews.
@@ -137,6 +159,7 @@ export const EnvelopeRenderProvider = ({
envelope,
envelopeItems: envelopeItemsFromProps,
fields,
+ signatures,
token,
presignToken,
recipients = [],
@@ -212,6 +235,7 @@ export const EnvelopeRenderProvider = ({
currentEnvelopeItem: currentItem,
setCurrentEnvelopeItem,
fields: fields ?? [],
+ signatures: signatures ?? [],
recipients,
getRecipientColorKey,
renderError,
diff --git a/packages/lib/universal/field-renderer/render-signature-field.ts b/packages/lib/universal/field-renderer/render-signature-field.ts
index c289770da..d13602b63 100644
--- a/packages/lib/universal/field-renderer/render-signature-field.ts
+++ b/packages/lib/universal/field-renderer/render-signature-field.ts
@@ -40,6 +40,52 @@ const getImageDimensions = (img: HTMLImageElement, fieldWidth: number, fieldHeig
};
};
+/**
+ * Build a Konva.Image for a base64 signature, sized to fit within the given
+ * field dimensions. Works in both browser and Node.js (via skia-canvas).
+ */
+const createSignatureImage = (
+ signatureImageAsBase64: string,
+ fieldWidth: number,
+ fieldHeight: number,
+): Konva.Image => {
+ if (typeof window !== 'undefined') {
+ const img = new Image();
+
+ const image = new Konva.Image({
+ image: img,
+ x: 0,
+ y: 0,
+ width: fieldWidth,
+ height: fieldHeight,
+ });
+
+ img.onload = () => {
+ image.setAttrs({
+ image: img,
+ ...getImageDimensions(img, fieldWidth, fieldHeight),
+ });
+ };
+
+ img.src = signatureImageAsBase64;
+
+ return image;
+ }
+
+ // Node.js with skia-canvas
+ if (!SkiaImage) {
+ throw new Error('Skia image not found');
+ }
+
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ const img = new SkiaImage(signatureImageAsBase64) as unknown as HTMLImageElement;
+
+ return new Konva.Image({
+ image: img,
+ ...getImageDimensions(img, fieldWidth, fieldHeight),
+ });
+};
+
const createFieldSignature = (
field: FieldToRender,
options: RenderFieldElementOptions,
@@ -67,6 +113,16 @@ const createFieldSignature = (
// Handle edit mode.
if (mode === 'edit') {
textToRender = fieldTypeName;
+
+ // If the field has already been signed and we have the signature data
+ // available, render it. Otherwise leave the field type label as a placeholder.
+ if (field.inserted && signature?.typedSignature) {
+ textToRender = signature.typedSignature;
+ }
+
+ if (field.inserted && signature?.signatureImageAsBase64) {
+ return createSignatureImage(signature.signatureImageAsBase64, fieldWidth, fieldHeight);
+ }
}
// Handle sign mode.
@@ -82,44 +138,7 @@ const createFieldSignature = (
}
if (signature?.signatureImageAsBase64) {
- if (typeof window !== 'undefined') {
- // Create a new HTML Image element
- const img = new Image();
-
- const image = new Konva.Image({
- image: img,
- x: 0,
- y: 0,
- width: fieldWidth,
- height: fieldHeight,
- });
-
- img.onload = () => {
- image.setAttrs({
- image: img,
- ...getImageDimensions(img, fieldWidth, fieldHeight),
- });
- };
-
- img.src = signature.signatureImageAsBase64;
-
- return image;
- } else {
- // Node.js with skia-canvas
- if (!SkiaImage) {
- throw new Error('Skia image not found');
- }
-
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
- const img = new SkiaImage(signature?.signatureImageAsBase64) as unknown as HTMLImageElement;
-
- const image = new Konva.Image({
- image: img,
- ...getImageDimensions(img, fieldWidth, fieldHeight),
- });
-
- return image;
- }
+ return createSignatureImage(signature.signatureImageAsBase64, fieldWidth, fieldHeight);
}
}
diff --git a/packages/trpc/server/envelope-router/envelope-fields/get-envelope-field-signatures.ts b/packages/trpc/server/envelope-router/envelope-fields/get-envelope-field-signatures.ts
new file mode 100644
index 000000000..4f3c5e7e1
--- /dev/null
+++ b/packages/trpc/server/envelope-router/envelope-fields/get-envelope-field-signatures.ts
@@ -0,0 +1,65 @@
+import { FieldType } from '@prisma/client';
+
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../../trpc';
+import {
+ ZGetEnvelopeFieldSignaturesRequestSchema,
+ ZGetEnvelopeFieldSignaturesResponseSchema,
+} from './get-envelope-field-signatures.types';
+
+export const getEnvelopeFieldSignaturesRoute = authenticatedProcedure
+ .input(ZGetEnvelopeFieldSignaturesRequestSchema)
+ .output(ZGetEnvelopeFieldSignaturesResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { teamId, user } = ctx;
+ const { envelopeId } = input;
+
+ ctx.logger.info({
+ input: {
+ envelopeId,
+ },
+ });
+
+ // Validate the user has access to the envelope.
+ const { envelopeWhereInput } = await getEnvelopeWhereInput({
+ id: {
+ type: 'envelopeId',
+ id: envelopeId,
+ },
+ type: null,
+ userId: user.id,
+ teamId,
+ });
+
+ const envelope = await prisma.envelope.findFirst({
+ where: envelopeWhereInput,
+ include: {
+ fields: {
+ where: {
+ inserted: true,
+ type: FieldType.SIGNATURE,
+ },
+ include: {
+ signature: true,
+ },
+ },
+ },
+ });
+
+ if (!envelope) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Envelope not found',
+ });
+ }
+
+ const signatures = envelope.fields.map((field) => ({
+ fieldId: field.id,
+ signatureImageAsBase64: field.signature?.signatureImageAsBase64 ?? null,
+ typedSignature: field.signature?.typedSignature ?? null,
+ }));
+
+ return signatures;
+ });
diff --git a/packages/trpc/server/envelope-router/envelope-fields/get-envelope-field-signatures.types.ts b/packages/trpc/server/envelope-router/envelope-fields/get-envelope-field-signatures.types.ts
new file mode 100644
index 000000000..d2fefaa17
--- /dev/null
+++ b/packages/trpc/server/envelope-router/envelope-fields/get-envelope-field-signatures.types.ts
@@ -0,0 +1,20 @@
+import { z } from 'zod';
+
+export const ZGetEnvelopeFieldSignaturesRequestSchema = z.object({
+ envelopeId: z.string().min(1),
+});
+
+export const ZGetEnvelopeFieldSignaturesResponseSchema = z
+ .object({
+ fieldId: z.number(),
+ signatureImageAsBase64: z.string().nullable(),
+ typedSignature: z.string().nullable(),
+ })
+ .array();
+
+export type TGetEnvelopeFieldSignaturesRequest = z.infer<
+ typeof ZGetEnvelopeFieldSignaturesRequestSchema
+>;
+export type TGetEnvelopeFieldSignaturesResponse = z.infer<
+ typeof ZGetEnvelopeFieldSignaturesResponseSchema
+>;
diff --git a/packages/trpc/server/envelope-router/router.ts b/packages/trpc/server/envelope-router/router.ts
index faa20d03f..83b8281b8 100644
--- a/packages/trpc/server/envelope-router/router.ts
+++ b/packages/trpc/server/envelope-router/router.ts
@@ -15,6 +15,7 @@ import { duplicateEnvelopeRoute } from './duplicate-envelope';
import { createEnvelopeFieldsRoute } from './envelope-fields/create-envelope-fields';
import { deleteEnvelopeFieldRoute } from './envelope-fields/delete-envelope-field';
import { getEnvelopeFieldRoute } from './envelope-fields/get-envelope-field';
+import { getEnvelopeFieldSignaturesRoute } from './envelope-fields/get-envelope-field-signatures';
import { updateEnvelopeFieldsRoute } from './envelope-fields/update-envelope-fields';
import { createEnvelopeRecipientsRoute } from './envelope-recipients/create-envelope-recipients';
import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient';
@@ -68,6 +69,7 @@ export const envelopeRouter = router({
},
field: {
get: getEnvelopeFieldRoute,
+ getSignatures: getEnvelopeFieldSignaturesRoute,
createMany: createEnvelopeFieldsRoute,
updateMany: updateEnvelopeFieldsRoute,
delete: deleteEnvelopeFieldRoute,