fix: improve field signing (#2830)

This commit is contained in:
David Nguyen
2026-06-01 19:48:20 +10:00
committed by GitHub
parent 4bda501d51
commit c50a01d004
5 changed files with 84 additions and 35 deletions
@@ -22,7 +22,7 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { type Field, FieldType, type Recipient, RecipientRole, type Signature, SigningStatus } from '@prisma/client'; import { type Field, FieldType, type Recipient, RecipientRole, type Signature, SigningStatus } from '@prisma/client';
import type Konva from 'konva'; import type Konva from 'konva';
import type { KonvaEventObject } from 'konva/lib/Node'; import type { KonvaEventObject } from 'konva/lib/Node';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context'; import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
@@ -57,17 +57,31 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
recipientFieldsRemaining, recipientFieldsRemaining,
showPendingFieldTooltip, showPendingFieldTooltip,
signField: signFieldInternal, signField: signFieldInternal,
email, email: emailState,
setEmail, setEmail,
fullName, fullName: fullNameState,
setFullName, setFullName,
signature, signature: signatureState,
setSignature, setSignature,
selectedAssistantRecipientFields, selectedAssistantRecipientFields,
selectedAssistantRecipient, selectedAssistantRecipient,
isDirectTemplate, isDirectTemplate,
} = useRequiredEnvelopeSigningContext(); } = useRequiredEnvelopeSigningContext();
// Note: We're using refs here due to the closure within the signField function.
const fullName = useRef(fullNameState);
const email = useRef(emailState);
const signature = useRef(signatureState);
useEffect(() => {
fullName.current = fullNameState;
email.current = emailState;
signature.current = signatureState;
}, [fullNameState, emailState, signatureState]);
const cachedRenderFields = useRef<Map<number, Field & { signature?: Signature | null }>>(new Map());
const prevShowPendingFieldTooltip = useRef(showPendingFieldTooltip);
const { onFieldSigned, onFieldUnsigned } = useEmbedSigningContext() || {}; const { onFieldSigned, onFieldUnsigned } = useEmbedSigningContext() || {};
const { stage, pageLayer, konvaContainer, unscaledViewport } = usePageRenderer( const { stage, pageLayer, konvaContainer, unscaledViewport } = usePageRenderer(
@@ -169,8 +183,8 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
return; return;
} }
let localEmail: string | null = email; let localEmail: string | null = email.current;
let localFullName: string | null = fullName; let localFullName: string | null = fullName.current;
let placeholderEmail: string | null = null; let placeholderEmail: string | null = null;
if (recipient.role === RecipientRole.ASSISTANT) { if (recipient.role === RecipientRole.ASSISTANT) {
@@ -180,7 +194,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
// Allows us let the user set a different email than their current logged in email. // Allows us let the user set a different email than their current logged in email.
if (isDirectTemplate) { if (isDirectTemplate) {
placeholderEmail = sessionData?.user?.email || email || recipient.email; placeholderEmail = sessionData?.user?.email || email.current || recipient.email;
if (!placeholderEmail || placeholderEmail === DIRECT_TEMPLATE_RECIPIENT_EMAIL) { if (!placeholderEmail || placeholderEmail === DIRECT_TEMPLATE_RECIPIENT_EMAIL) {
placeholderEmail = null; placeholderEmail = null;
@@ -205,7 +219,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
return; return;
} }
handleCheckboxFieldClick({ field, clickedCheckboxIndex }) void handleCheckboxFieldClick({ field, clickedCheckboxIndex })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
@@ -243,7 +257,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
* NUMBER FIELD. * NUMBER FIELD.
*/ */
.with({ type: FieldType.NUMBER }, (field) => { .with({ type: FieldType.NUMBER }, (field) => {
handleNumberFieldClick({ field, number: null }) void handleNumberFieldClick({ field, number: null })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
@@ -258,7 +272,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
* TEXT FIELD. * TEXT FIELD.
*/ */
.with({ type: FieldType.TEXT }, (field) => { .with({ type: FieldType.TEXT }, (field) => {
handleTextFieldClick({ field, text: null }) void handleTextFieldClick({ field, text: null })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
@@ -273,7 +287,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
* EMAIL FIELD. * EMAIL FIELD.
*/ */
.with({ type: FieldType.EMAIL }, (field) => { .with({ type: FieldType.EMAIL }, (field) => {
handleEmailFieldClick({ field, email: localEmail, placeholderEmail }) void handleEmailFieldClick({ field, email: localEmail, placeholderEmail })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
@@ -294,7 +308,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
.with({ type: FieldType.INITIALS }, (field) => { .with({ type: FieldType.INITIALS }, (field) => {
const initials = localFullName ? extractInitials(localFullName) : null; const initials = localFullName ? extractInitials(localFullName) : null;
handleInitialsFieldClick({ field, initials }) void handleInitialsFieldClick({ field, initials })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
@@ -309,7 +323,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
* NAME FIELD. * NAME FIELD.
*/ */
.with({ type: FieldType.NAME }, (field) => { .with({ type: FieldType.NAME }, (field) => {
handleNameFieldClick({ field, name: localFullName }) void handleNameFieldClick({ field, name: localFullName })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
@@ -328,7 +342,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
* DROPDOWN FIELD. * DROPDOWN FIELD.
*/ */
.with({ type: FieldType.DROPDOWN }, (field) => { .with({ type: FieldType.DROPDOWN }, (field) => {
handleDropdownFieldClick({ field, text: null }) void handleDropdownFieldClick({ field, text: null })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
@@ -356,20 +370,23 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
* SIGNATURE FIELD. * SIGNATURE FIELD.
*/ */
.with({ type: FieldType.SIGNATURE }, (field) => { .with({ type: FieldType.SIGNATURE }, (field) => {
handleSignatureFieldClick({ void handleSignatureFieldClick({
field, field,
fullName, fullName: fullName.current,
signature, signature: signature.current,
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled, typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled, uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled, drawSignatureEnabled: envelope.documentMeta.drawSignatureEnabled,
}) })
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (!payload) {
return;
}
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
if (payload.value) { if (payload.value) {
void executeActionAuthProcedure({ await executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => { onReauthFormSubmit: async (authOptions) => {
await signField(field.id, payload, authOptions); await signField(field.id, payload, authOptions);
@@ -382,7 +399,6 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
} else { } else {
await signField(field.id, payload); await signField(field.id, payload);
} }
}
}) })
.finally(() => { .finally(() => {
loadingSpinnerGroup.destroy(); loadingSpinnerGroup.destroy();
@@ -410,15 +426,26 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
return; return;
} }
// Render current recipient fields. // Render current recipient fields which have changed or are not currently rendered.
for (const field of localPageFields) { for (const field of localPageFields) {
const existingCachedField = cachedRenderFields.current.get(field.id);
const isFieldCurrentlyRendered = pageLayer.current.findOne(`#${field.id}`);
if (
!isFieldCurrentlyRendered ||
!existingCachedField ||
existingCachedField.inserted !== field.inserted ||
existingCachedField.customText !== field.customText
) {
renderFieldOnLayer(field); renderFieldOnLayer(field);
cachedRenderFields.current.set(field.id, field);
}
} }
// Render other recipient signed and inserted fields. // Render other recipient signed and inserted fields.
for (const field of localPageOtherRecipientFields) { for (const field of localPageOtherRecipientFields) {
try { try {
renderField({ const { fieldGroup } = renderField({
scale, scale,
pageLayer: pageLayer.current, pageLayer: pageLayer.current,
field: { field: {
@@ -437,6 +464,11 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
editable: false, editable: false,
mode: 'sign', mode: 'sign',
}); });
// Other-recipient fields are display-only — they have no click handlers
// and shouldn't intercept events meant for the current recipient's
// fields. Disable hit detection on the entire group.
fieldGroup.listening(false);
} catch (err) { } catch (err) {
console.error('Unable to render one or more fields belonging to other recipients.'); console.error('Unable to render one or more fields belonging to other recipients.');
console.error(err); console.error(err);
@@ -488,10 +520,19 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
return; return;
} }
// When the pending-field tooltip toggles, all unsigned required fields need to
// be re-rendered so their stroke color updates (green <-> orange). Field-level
// properties like `inserted` and `customText` haven't changed, so the cache
// would otherwise skip them — clear it to force a fresh render.
if (prevShowPendingFieldTooltip.current !== showPendingFieldTooltip) {
cachedRenderFields.current.clear();
prevShowPendingFieldTooltip.current = showPendingFieldTooltip;
}
renderFields(); renderFields();
pageLayer.current.batchDraw(); pageLayer.current.batchDraw();
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]); }, [localPageFields, showPendingFieldTooltip]);
/** /**
* Rerender the whole page if the selected assistant recipient changes. * Rerender the whole page if the selected assistant recipient changes.
@@ -503,6 +544,7 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
// Rerender the whole page. // Rerender the whole page.
pageLayer.current.destroyChildren(); pageLayer.current.destroyChildren();
cachedRenderFields.current.clear();
renderFields(); renderFields();
@@ -69,6 +69,7 @@ export const upsertFieldRect = (field: FieldToRender, options: RenderFieldElemen
export const createSpinner = ({ fieldWidth, fieldHeight }: { fieldWidth: number; fieldHeight: number }) => { export const createSpinner = ({ fieldWidth, fieldHeight }: { fieldWidth: number; fieldHeight: number }) => {
const loadingGroup = new Konva.Group({ const loadingGroup = new Konva.Group({
name: 'loading-spinner-group', name: 'loading-spinner-group',
listening: false,
}); });
const rect = new Konva.Rect({ const rect = new Konva.Rect({
@@ -107,6 +107,7 @@ export const renderDropdownFieldElement = (field: FieldToRender, options: Render
fontFamily: konvaTextFontFamily, fontFamily: konvaTextFontFamily,
fill: konvaTextFill, fill: konvaTextFill,
verticalAlign: 'middle', verticalAlign: 'middle',
listening: false,
}); });
const arrow = new Konva.Line({ const arrow = new Konva.Line({
@@ -120,6 +121,7 @@ export const renderDropdownFieldElement = (field: FieldToRender, options: Render
lineJoin: 'round', lineJoin: 'round',
closed: false, closed: false,
visible: mode !== 'export', visible: mode !== 'export',
listening: false,
}); });
fieldGroup.add(selectedText); fieldGroup.add(selectedText);
@@ -36,6 +36,7 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
new Konva.Text({ new Konva.Text({
id: `${field.renderId}-text`, id: `${field.renderId}-text`,
name: 'field-text', name: 'field-text',
listening: false,
}); });
// Calculate text positioning based on alignment // Calculate text positioning based on alignment
@@ -77,6 +77,7 @@ const createSignatureImage = (signatureImageAsBase64: string, fieldWidth: number
y: 0, y: 0,
width: fieldWidth, width: fieldWidth,
height: fieldHeight, height: fieldHeight,
listening: false,
}); });
img.onload = () => { img.onload = () => {
@@ -109,6 +110,7 @@ const createSignatureImage = (signatureImageAsBase64: string, fieldWidth: number
return new Konva.Image({ return new Konva.Image({
image: img, image: img,
...getImageDimensions(img, fieldWidth, fieldHeight), ...getImageDimensions(img, fieldWidth, fieldHeight),
listening: false,
}); });
}; };
@@ -121,6 +123,7 @@ const createFieldSignature = (field: FieldToRender, options: RenderFieldElementO
const fieldText = new Konva.Text({ const fieldText = new Konva.Text({
id: `${field.renderId}-text`, id: `${field.renderId}-text`,
name: 'field-text', name: 'field-text',
listening: false,
}); });
const fieldTypeName = translations?.[field.type] || field.type; const fieldTypeName = translations?.[field.type] || field.type;