feat: prefill typed signature with user's full name (#2324)

Add fullName prop to signature pad components to automatically populate
typed signature
field with signer's name. Updates signature dialog, type component, and
all signing forms
across embed, document, template, and envelope flows to pass through the
user's full
name for better user experience.
This commit is contained in:
Lucas Smith
2025-12-16 12:06:04 +11:00
committed by GitHub
parent 8462cd13fd
commit f0a5a7e816
14 changed files with 75 additions and 26 deletions
@@ -17,6 +17,7 @@ import { DocumentSigningDisclosure } from '../general/document-signing/document-
export type SignFieldSignatureDialogProps = {
initialSignature?: string;
fullName?: string;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
@@ -28,6 +29,7 @@ export const SignFieldSignatureDialog = createCallable<
>(
({
call,
fullName,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
@@ -46,6 +48,7 @@ export const SignFieldSignatureDialog = createCallable<
</DialogHeader>
<SignaturePad
fullName={fullName}
value={localSignature ?? ''}
onChange={({ value }) => setLocalSignature(value)}
typedSignatureEnabled={typedSignatureEnabled}
@@ -438,6 +438,7 @@ export const EmbedDirectTemplateClientPage = ({
className="mt-2"
disabled={isThrottled || isSubmitting}
disableAnimation
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
@@ -455,6 +455,7 @@ export const EmbedSignDocumentV1ClientPage = ({
className="mt-2"
disabled={isThrottled || isSubmitting}
disableAnimation
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
@@ -319,6 +319,7 @@ export const MultiSignDocumentSigningView = ({
className="mt-2"
disabled={isSubmitting}
disableAnimation
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={
+2 -1
View File
@@ -110,7 +110,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
<Label htmlFor="email" className="text-muted-foreground">
<Trans>Email</Trans>
</Label>
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
<Input id="email" type="email" className="mt-2 bg-muted" value={user.email} disabled />
</div>
<FormField
@@ -124,6 +124,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
<FormControl>
<SignaturePadDialog
disabled={isSubmitting}
fullName={user.name ?? ''}
value={value}
onChange={(v) => onChange(v ?? '')}
/>
@@ -417,6 +417,7 @@ export const DirectTemplateSigningForm = ({
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
fullName={fullName}
value={signature ?? ''}
onChange={(value) => setSignature(value)}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
@@ -433,7 +434,7 @@ export const DirectTemplateSigningForm = ({
<div className="mt-4 flex gap-x-4">
<Button
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
className="w-full bg-black/5 hover:bg-black/10 dark:bg-muted dark:hover:bg-muted/80"
size="lg"
variant="secondary"
disabled={isSubmitting}
@@ -280,6 +280,7 @@ export const DocumentSigningForm = ({
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
@@ -56,8 +56,11 @@ export const DocumentSigningSignatureField = ({
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(2);
const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredDocumentSigningContext();
const {
fullName,
signature: providedSignature,
setSignature: setProvidedSignature,
} = useRequiredDocumentSigningContext();
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
@@ -236,13 +239,13 @@ export const DocumentSigningSignatureField = ({
type="Signature"
>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
<div className="absolute inset-0 flex items-center justify-center rounded-md bg-background">
<Loader className="h-5 w-5 animate-spin text-primary md:h-8 md:w-8" />
</div>
)}
{state === 'empty' && (
<p className="group-hover:text-primary font-signature text-muted-foreground group-hover:text-recipient-green text-[clamp(0.575rem,25cqw,1.2rem)] text-xl duration-200">
<p className="font-signature text-[clamp(0.575rem,25cqw,1.2rem)] text-xl text-muted-foreground duration-200 group-hover:text-primary group-hover:text-recipient-green">
<Trans>Signature</Trans>
</p>
)}
@@ -259,7 +262,7 @@ export const DocumentSigningSignatureField = ({
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
<p
ref={signatureRef}
className="font-signature text-muted-foreground w-full overflow-hidden break-all text-center leading-tight duration-200"
className="w-full overflow-hidden break-all text-center font-signature leading-tight text-muted-foreground duration-200"
style={{ fontSize: `${fontSize}rem` }}
>
{signature?.typedSignature}
@@ -272,12 +275,13 @@ export const DocumentSigningSignatureField = ({
<DialogTitle>
<Trans>
Sign as {recipient.name}{' '}
<div className="text-muted-foreground h-5">({recipient.email})</div>
<div className="h-5 text-muted-foreground">({recipient.email})</div>
</Trans>
</DialogTitle>
<SignaturePad
className="mt-2"
fullName={fullName}
value={localSignature ?? ''}
onChange={({ value }) => setLocalSignature(value)}
typedSignatureEnabled={typedSignatureEnabled}
@@ -41,7 +41,7 @@ export default function EnvelopeSignerForm() {
if (recipient.role === RecipientRole.ASSISTANT) {
return (
<fieldset className="embed--DocumentWidgetForm dark:bg-background border-border rounded-2xl sm:border sm:p-3">
<fieldset className="embed--DocumentWidgetForm rounded-2xl border-border sm:border sm:p-3 dark:bg-background">
<RadioGroup
className="gap-0 space-y-2 shadow-none sm:space-y-3"
value={selectedAssistantRecipient?.id?.toString()}
@@ -54,7 +54,7 @@ export default function EnvelopeSignerForm() {
.map((r) => (
<div
key={r.id}
className="bg-widget border-border relative flex flex-col gap-4 rounded-lg border p-4"
className="relative flex flex-col gap-4 rounded-lg border border-border bg-widget p-4"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -69,15 +69,15 @@ export default function EnvelopeSignerForm() {
{r.name}
{r.id === recipient.id && (
<span className="text-muted-foreground ml-2">
<span className="ml-2 text-muted-foreground">
<Trans>(You)</Trans>
</span>
)}
</Label>
<p className="text-muted-foreground text-xs">{r.email}</p>
<p className="text-xs text-muted-foreground">{r.email}</p>
</div>
</div>
<div className="text-muted-foreground text-xs leading-[inherit]">
<div className="text-xs leading-[inherit] text-muted-foreground">
<Plural
value={assistantFields.filter((field) => field.recipientId === r.id).length}
one="# field"
@@ -103,7 +103,7 @@ export default function EnvelopeSignerForm() {
<Input
type="text"
id="full-name"
className="bg-background mt-2"
className="mt-2 bg-background"
value={fullName}
disabled={isNameLocked}
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
@@ -119,6 +119,7 @@ export default function EnvelopeSignerForm() {
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
fullName={fullName}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={envelope.documentMeta.typedSignatureEnabled}
@@ -374,6 +374,7 @@ export default function EnvelopeSignerPageRenderer() {
.with({ type: FieldType.SIGNATURE }, (field) => {
handleSignatureFieldClick({
field,
fullName,
signature,
typedSignatureEnabled: envelope.documentMeta.typedSignatureEnabled,
uploadSignatureEnabled: envelope.documentMeta.uploadSignatureEnabled,
@@ -8,6 +8,7 @@ import { SignFieldSignatureDialog } from '~/components/dialogs/sign-field-signat
type HandleSignatureFieldClickOptions = {
field: TFieldSignature;
fullName?: string;
signature: string | null;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
@@ -17,8 +18,14 @@ type HandleSignatureFieldClickOptions = {
export const handleSignatureFieldClick = async (
options: HandleSignatureFieldClickOptions,
): Promise<Extract<TSignEnvelopeFieldValue, { type: typeof FieldType.SIGNATURE }> | null> => {
const { field, signature, typedSignatureEnabled, uploadSignatureEnabled, drawSignatureEnabled } =
options;
const {
field,
fullName,
signature,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
} = options;
if (field.type !== FieldType.SIGNATURE) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
@@ -37,6 +44,7 @@ export const handleSignatureFieldClick = async (
if (!signatureToInsert) {
signatureToInsert = await SignFieldSignatureDialog.call({
fullName,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
@@ -15,6 +15,7 @@ import { SignatureRender } from './signature-render';
export type SignaturePadDialogProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
disabled?: boolean;
fullName?: string;
value?: string;
onChange: (_value: string) => void;
dialogConfirmText?: MessageDescriptor | string;
@@ -26,6 +27,7 @@ export type SignaturePadDialogProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'o
export const SignaturePadDialog = ({
className,
fullName,
value,
onChange,
disabled = false,
@@ -43,7 +45,7 @@ export const SignaturePadDialog = ({
return (
<div
className={cn(
'aspect-signature-pad bg-background relative block w-full select-none rounded-lg border',
'relative block aspect-signature-pad w-full select-none rounded-lg border bg-background',
className,
{
'pointer-events-none opacity-50': disabled,
@@ -112,6 +114,7 @@ export const SignaturePadDialog = ({
<DialogContent hideClose={true} className="p-6 pt-4">
<SignaturePad
id="signature"
fullName={fullName}
value={value}
className={className}
disabled={disabled}
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useRef } from 'react';
import { useLingui } from '@lingui/react/macro';
@@ -7,13 +7,27 @@ import { cn } from '../../lib/utils';
export type SignaturePadTypeProps = {
className?: string;
value?: string;
defaultValue?: string;
onChange: (_value: string) => void;
};
export const SignaturePadType = ({ className, value, onChange }: SignaturePadTypeProps) => {
export const SignaturePadType = ({
className,
value,
defaultValue,
onChange,
}: SignaturePadTypeProps) => {
const { t } = useLingui();
const $isDirty = useRef(false);
// Colors don't actually work for text.
const [selectedColor, setSelectedColor] = useState('black');
useEffect(() => {
if (!$isDirty.current && !value && defaultValue) {
$isDirty.current = true;
onChange(defaultValue);
}
}, [defaultValue, value, onChange]);
return (
<div className={cn('flex h-full w-full items-center justify-center', className)}>
@@ -23,7 +37,10 @@ export const SignaturePadType = ({ className, value, onChange }: SignaturePadTyp
className="w-full bg-transparent px-4 text-center font-signature text-7xl text-black placeholder:text-4xl focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white"
// style={{ color: selectedColor }}
value={value}
onChange={(event) => onChange(event.target.value.trimStart())}
onChange={(event) => {
onChange(event.target.value.trimStart());
$isDirty.current = true;
}}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
@@ -21,6 +21,7 @@ export type SignaturePadValue = {
};
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
fullName?: string;
value?: string;
onChange?: (_value: SignaturePadValue) => void;
@@ -34,6 +35,7 @@ export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChang
};
export const SignaturePad = ({
fullName,
value = '',
onChange,
disabled = false,
@@ -168,7 +170,7 @@ export const SignaturePad = ({
<TabsContent
value="draw"
className="border-border aspect-signature-pad dark:bg-background relative flex items-center justify-center rounded-md border bg-neutral-50 text-center"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-neutral-50 text-center dark:bg-background"
>
<SignaturePadDraw
className="h-full w-full"
@@ -179,15 +181,19 @@ export const SignaturePad = ({
<TabsContent
value="text"
className="border-border aspect-signature-pad dark:bg-background relative flex items-center justify-center rounded-md border bg-neutral-50 text-center"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-neutral-50 text-center dark:bg-background"
>
<SignaturePadType value={typedSignature} onChange={onTypedSignatureChange} />
<SignaturePadType
value={typedSignature}
defaultValue={fullName}
onChange={onTypedSignatureChange}
/>
</TabsContent>
<TabsContent
value="image"
className={cn(
'border-border aspect-signature-pad dark:bg-background relative rounded-md border bg-neutral-50',
'relative aspect-signature-pad rounded-md border border-border bg-neutral-50 dark:bg-background',
{
'bg-white': imageSignature,
},