mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
fix: improve field signing (#2830)
This commit is contained in:
+65
-23
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user