Files
sign/apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx

318 lines
9.7 KiB
TypeScript
Raw Normal View History

2025-01-02 15:33:37 +11:00
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
2023-08-17 19:56:18 +10:00
2025-01-02 15:33:37 +11:00
import { msg } from '@lingui/core/macro';
2024-08-27 20:34:39 +09:00
import { useLingui } from '@lingui/react';
2025-01-02 15:33:37 +11:00
import { Trans } from '@lingui/react/macro';
2023-08-17 19:56:18 +10:00
import { Loader } from 'lucide-react';
2025-01-02 15:33:37 +11:00
import { useRevalidator } from 'react-router';
2023-08-17 19:56:18 +10:00
fix: update document flow fetch logic (#1039) ## Description **Fixes issues with mismatching state between document steps.** For example, editing a recipient and proceeding to the next step may not display the updated recipient. And going back will display the old recipient instead of the updated values. **This PR also improves mutation and query speeds by adding logic to bypass query invalidation.** ```ts export const trpc = createTRPCReact<AppRouter>({ unstable_overrides: { useMutation: { async onSuccess(opts) { await opts.originalFn(); // This forces mutations to wait for all the queries on the page to reload, and in // this case one of the queries is `searchDocument` for the command overlay, which // on average takes ~500ms. This means that every single mutation must wait for this. await opts.queryClient.invalidateQueries(); }, }, }, }); ``` I've added workarounds to allow us to bypass things such as batching and invalidating queries. But I think we should instead remove this and update all the mutations where a query is required for a more optimised system. ## Example benchmarks Using stg-app vs this preview there's an average 50% speed increase across mutations. **Set signer step:** Average old speed: ~1100ms Average new speed: ~550ms **Set recipient step:** Average old speed: ~1200ms Average new speed: ~600ms **Set fields step:** Average old speed: ~1200ms Average new speed: ~600ms ## Related Issue This will resolve #470 ## Changes Made - Added ability to skip batch queries - Added a state to store the required document data. - Refetch the data between steps if/when required - Optimise mutations and queries ## Checklist - [X] I have tested these changes locally and they work as expected. - [X] I have followed the project's coding style guidelines. --------- Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-03-26 21:12:41 +08:00
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
2024-03-28 13:13:29 +08:00
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
2023-12-02 11:09:42 +11:00
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
2023-08-17 19:56:18 +10:00
import { trpc } from '@documenso/trpc/react';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
2023-08-17 19:56:18 +10:00
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
2023-08-17 19:56:18 +10:00
import { useToast } from '@documenso/ui/primitives/use-toast';
2025-01-02 15:33:37 +11:00
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
2024-04-02 14:16:36 +07:00
2025-01-02 15:33:37 +11:00
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { useRequiredDocumentSigningContext } from './document-signing-provider';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
2023-08-17 19:56:18 +10:00
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
2025-01-02 15:33:37 +11:00
export type DocumentSigningSignatureFieldProps = {
2023-08-17 19:56:18 +10:00
field: FieldWithSignature;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
2023-08-17 19:56:18 +10:00
};
2025-01-02 15:33:37 +11:00
export const DocumentSigningSignatureField = ({
field,
onSignField,
onUnsignField,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
2025-01-02 15:33:37 +11:00
}: DocumentSigningSignatureFieldProps) => {
2024-08-27 20:34:39 +09:00
const { _ } = useLingui();
2023-08-17 19:56:18 +10:00
const { toast } = useToast();
2025-01-02 15:33:37 +11:00
const { revalidate } = useRevalidator();
2024-03-28 13:13:29 +08:00
const { recipient } = useDocumentSigningRecipientContext();
const signatureRef = useRef<HTMLParagraphElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(2);
const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredDocumentSigningContext();
2024-03-28 13:13:29 +08:00
2025-01-02 15:33:37 +11:00
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
2023-08-17 19:56:18 +10:00
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
fix: update document flow fetch logic (#1039) ## Description **Fixes issues with mismatching state between document steps.** For example, editing a recipient and proceeding to the next step may not display the updated recipient. And going back will display the old recipient instead of the updated values. **This PR also improves mutation and query speeds by adding logic to bypass query invalidation.** ```ts export const trpc = createTRPCReact<AppRouter>({ unstable_overrides: { useMutation: { async onSuccess(opts) { await opts.originalFn(); // This forces mutations to wait for all the queries on the page to reload, and in // this case one of the queries is `searchDocument` for the command overlay, which // on average takes ~500ms. This means that every single mutation must wait for this. await opts.queryClient.invalidateQueries(); }, }, }, }); ``` I've added workarounds to allow us to bypass things such as batching and invalidating queries. But I think we should instead remove this and update all the mutations where a query is required for a more optimised system. ## Example benchmarks Using stg-app vs this preview there's an average 50% speed increase across mutations. **Set signer step:** Average old speed: ~1100ms Average new speed: ~550ms **Set recipient step:** Average old speed: ~1200ms Average new speed: ~600ms **Set fields step:** Average old speed: ~1200ms Average new speed: ~600ms ## Related Issue This will resolve #470 ## Changes Made - Added ability to skip batch queries - Added a state to store the required document data. - Refetch the data between steps if/when required - Optimise mutations and queries ## Checklist - [X] I have tested these changes locally and they work as expected. - [X] I have followed the project's coding style guidelines. --------- Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-03-26 21:12:41 +08:00
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
2023-08-17 19:56:18 +10:00
const {
mutateAsync: removeSignedFieldWithToken,
isPending: isRemoveSignedFieldWithTokenLoading,
fix: update document flow fetch logic (#1039) ## Description **Fixes issues with mismatching state between document steps.** For example, editing a recipient and proceeding to the next step may not display the updated recipient. And going back will display the old recipient instead of the updated values. **This PR also improves mutation and query speeds by adding logic to bypass query invalidation.** ```ts export const trpc = createTRPCReact<AppRouter>({ unstable_overrides: { useMutation: { async onSuccess(opts) { await opts.originalFn(); // This forces mutations to wait for all the queries on the page to reload, and in // this case one of the queries is `searchDocument` for the command overlay, which // on average takes ~500ms. This means that every single mutation must wait for this. await opts.queryClient.invalidateQueries(); }, }, }, }); ``` I've added workarounds to allow us to bypass things such as batching and invalidating queries. But I think we should instead remove this and update all the mutations where a query is required for a more optimised system. ## Example benchmarks Using stg-app vs this preview there's an average 50% speed increase across mutations. **Set signer step:** Average old speed: ~1100ms Average new speed: ~550ms **Set recipient step:** Average old speed: ~1200ms Average new speed: ~600ms **Set fields step:** Average old speed: ~1200ms Average new speed: ~600ms ## Related Issue This will resolve #470 ## Changes Made - Added ability to skip batch queries - Added a state to store the required document data. - Refetch the data between steps if/when required - Optimise mutations and queries ## Checklist - [X] I have tested these changes locally and they work as expected. - [X] I have followed the project's coding style guidelines. --------- Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2024-03-26 21:12:41 +08:00
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
2023-08-17 19:56:18 +10:00
2025-01-13 13:41:53 +11:00
const { signature } = field;
2023-08-17 19:56:18 +10:00
2025-01-02 15:33:37 +11:00
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
2023-08-17 19:56:18 +10:00
const [showSignatureModal, setShowSignatureModal] = useState(false);
const [localSignature, setLocalSignature] = useState<string | null>(null);
const state = useMemo<SignatureFieldState>(() => {
if (!field.inserted) {
return 'empty';
}
if (signature?.signatureImageAsBase64) {
return 'signed-image';
}
return 'signed-text';
}, [field.inserted, signature?.signatureImageAsBase64]);
2024-03-28 13:13:29 +08:00
const onPreSign = () => {
if (!providedSignature) {
2024-03-28 13:13:29 +08:00
setShowSignatureModal(true);
return false;
}
2024-03-28 13:13:29 +08:00
return true;
};
/**
* When the user clicks the sign button in the dialog where they enter their signature.
*/
const onDialogSignClick = () => {
setShowSignatureModal(false);
setProvidedSignature(localSignature);
2024-03-28 13:13:29 +08:00
if (!localSignature) {
return;
}
2023-08-17 19:56:18 +10:00
2024-03-28 13:13:29 +08:00
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions, localSignature),
actionTarget: field.type,
});
};
2024-03-28 13:13:29 +08:00
const onSign = async (authOptions?: TRecipientActionAuth, signature?: string) => {
try {
const value = signature || providedSignature;
2023-12-02 11:09:42 +11:00
if (!value) {
2024-03-28 13:13:29 +08:00
setShowSignatureModal(true);
2023-12-02 11:09:42 +11:00
return;
}
const isTypedSignature = !value.startsWith('data:image');
if (isTypedSignature && typedSignatureEnabled === false) {
toast({
title: _(msg`Error`),
description: _(msg`Typed signatures are not allowed. Please draw your signature.`),
variant: 'destructive',
});
return;
}
const payload: TSignFieldWithTokenMutationSchema = {
2023-08-17 19:56:18 +10:00
token: recipient.token,
fieldId: field.id,
2023-12-02 11:09:42 +11:00
value,
isBase64: !isTypedSignature,
2024-03-28 13:13:29 +08:00
authOptions,
};
if (onSignField) {
await onSignField(payload);
2025-01-02 15:33:37 +11:00
} else {
await signFieldWithToken(payload);
}
2025-01-02 15:33:37 +11:00
await revalidate();
2023-08-17 19:56:18 +10:00
} catch (err) {
2024-03-28 13:13:29 +08:00
const error = AppError.parseError(err);
if (error.code === AppErrorCode.UNAUTHORIZED) {
throw error;
}
2023-08-17 19:56:18 +10:00
console.error(err);
toast({
2024-08-27 20:34:39 +09:00
title: _(msg`Error`),
description: _(msg`An error occurred while signing the document.`),
2023-08-17 19:56:18 +10:00
variant: 'destructive',
});
}
};
const onRemove = async () => {
try {
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
2023-08-17 19:56:18 +10:00
token: recipient.token,
fieldId: field.id,
};
if (onUnsignField) {
await onUnsignField(payload);
return;
2025-01-02 15:33:37 +11:00
} else {
await removeSignedFieldWithToken(payload);
}
2025-01-02 15:33:37 +11:00
await revalidate();
2023-08-17 19:56:18 +10:00
} catch (err) {
console.error(err);
toast({
2024-08-27 20:34:39 +09:00
title: _(msg`Error`),
description: _(msg`An error occurred while removing the signature.`),
2023-08-17 19:56:18 +10:00
variant: 'destructive',
});
}
};
useLayoutEffect(() => {
if (!signatureRef.current || !containerRef.current || !signature?.typedSignature) {
return;
}
const adjustTextSize = () => {
const container = containerRef.current;
const text = signatureRef.current;
if (!container || !text) {
return;
}
let size = 2;
text.style.fontSize = `${size}rem`;
while (
(text.scrollWidth > container.clientWidth || text.scrollHeight > container.clientHeight) &&
size > 0.8
) {
size -= 0.1;
text.style.fontSize = `${size}rem`;
}
setFontSize(size);
};
const resizeObserver = new ResizeObserver(adjustTextSize);
resizeObserver.observe(containerRef.current);
adjustTextSize();
return () => resizeObserver.disconnect();
}, [signature?.typedSignature]);
2023-08-17 19:56:18 +10:00
return (
2025-01-02 15:33:37 +11:00
<DocumentSigningFieldContainer
2024-03-28 13:13:29 +08:00
field={field}
onPreSign={onPreSign}
onSign={onSign}
onRemove={onRemove}
type="Signature"
>
2023-08-17 19:56:18 +10:00
{isLoading && (
2023-09-23 13:25:39 +10:00
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
2023-08-17 19:56:18 +10:00
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{state === 'empty' && (
<p className="group-hover:text-primary font-signature text-muted-foreground text-[clamp(0.575rem,25cqw,1.2rem)] text-xl duration-200 group-hover:text-yellow-300">
2024-08-27 20:34:39 +09:00
<Trans>Signature</Trans>
2023-08-17 19:56:18 +10:00
</p>
)}
{state === 'signed-image' && signature?.signatureImageAsBase64 && (
<img
src={signature.signatureImageAsBase64}
alt={`Signature for ${recipient.name}`}
className="h-full w-full object-contain"
2023-08-17 19:56:18 +10:00
/>
)}
2023-08-31 14:06:19 +10:00
{state === 'signed-text' && (
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
<p
ref={signatureRef}
className="font-signature text-muted-foreground dark:text-background w-full overflow-hidden break-all text-center leading-tight duration-200"
style={{ fontSize: `${fontSize}rem` }}
>
{signature?.typedSignature}
</p>
</div>
2023-08-17 19:56:18 +10:00
)}
<Dialog open={showSignatureModal} onOpenChange={setShowSignatureModal}>
<DialogContent>
<DialogTitle>
2024-08-27 20:34:39 +09:00
<Trans>
Sign as {recipient.name}{' '}
<div className="text-muted-foreground h-5">({recipient.email})</div>
</Trans>
2023-08-17 19:56:18 +10:00
</DialogTitle>
<SignaturePad
className="mt-2"
value={localSignature ?? ''}
onChange={({ value }) => setLocalSignature(value)}
typedSignatureEnabled={typedSignatureEnabled}
uploadSignatureEnabled={uploadSignatureEnabled}
drawSignatureEnabled={drawSignatureEnabled}
/>
2023-08-17 19:56:18 +10:00
2025-01-02 15:33:37 +11:00
<DocumentSigningDisclosure />
2023-08-17 19:56:18 +10:00
<DialogFooter>
<div className="flex w-full flex-1 flex-nowrap gap-4">
<Button
type="button"
2024-08-27 20:34:39 +09:00
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
2023-08-17 19:56:18 +10:00
variant="secondary"
onClick={() => {
setShowSignatureModal(false);
setLocalSignature(null);
}}
>
2024-08-27 20:34:39 +09:00
<Trans>Cancel</Trans>
2023-08-17 19:56:18 +10:00
</Button>
<Button
type="button"
className="flex-1"
disabled={!localSignature}
2024-03-28 13:13:29 +08:00
onClick={() => onDialogSignClick()}
2023-08-17 19:56:18 +10:00
>
2024-08-27 20:34:39 +09:00
<Trans>Sign</Trans>
2023-08-17 19:56:18 +10:00
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
2025-01-02 15:33:37 +11:00
</DocumentSigningFieldContainer>
2023-08-17 19:56:18 +10:00
);
};