feat: add qr signatures

This commit is contained in:
David Nguyen
2026-08-18 11:49:15 +10:00
parent 779de01fe8
commit 9825ea88b1
99 changed files with 1686 additions and 48 deletions
@@ -1,15 +1,16 @@
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import type { TQrSignatureContext } from '@documenso/lib/types/qr-signature';
import { Trans } from '@lingui/react/macro';
import { KeyboardIcon, UploadCloudIcon } from 'lucide-react';
import { KeyboardIcon, SmartphoneIcon, UploadCloudIcon } from 'lucide-react';
import type { HTMLAttributes } from 'react';
import { useState } from 'react';
import { match } from 'ts-pattern';
import { match, P } from 'ts-pattern';
import { SignatureIcon } from '../../icons/signature';
import { cn } from '../../lib/utils';
import { SignaturePadDraw } from './signature-pad-draw';
import type { QrSignatureSession } from './signature-pad-qr';
import { SignaturePadQr } from './signature-pad-qr';
import { SignaturePadType } from './signature-pad-type';
import { SignaturePadUpload } from './signature-pad-upload';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './signature-tabs';
@@ -29,6 +30,8 @@ export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChang
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
qrSignatureEnabled?: boolean;
qrSignatureContext?: TQrSignatureContext;
onValidityChange?: (isValid: boolean) => void;
};
@@ -41,11 +44,15 @@ export const SignaturePad = ({
typedSignatureEnabled = true,
uploadSignatureEnabled = true,
drawSignatureEnabled = true,
qrSignatureEnabled = true,
qrSignatureContext,
}: SignaturePadProps) => {
const [imageSignature, setImageSignature] = useState(isBase64Image(value) ? value : '');
const [drawSignature, setDrawSignature] = useState(isBase64Image(value) ? value : '');
const [typedSignature, setTypedSignature] = useState(isBase64Image(value) ? '' : value);
const [qrSession, setQrSession] = useState<QrSignatureSession | null>(null);
/**
* This is cooked.
*
@@ -53,7 +60,7 @@ export const SignaturePad = ({
* the first enabled tab.
*/
const [tab, setTab] = useState(
((): 'draw' | 'text' | 'image' => {
((): 'draw' | 'text' | 'image' | 'qr' => {
// First passthrough to check to see if there's a signature for a given tab.
if (drawSignatureEnabled && drawSignature) {
return 'draw';
@@ -80,6 +87,10 @@ export const SignaturePad = ({
return 'image';
}
if (qrSignatureEnabled) {
return 'qr';
}
throw new Error('No signature enabled');
})(),
);
@@ -111,7 +122,7 @@ export const SignaturePad = ({
});
};
const onTabChange = (value: 'draw' | 'text' | 'image') => {
const onTabChange = (value: 'draw' | 'text' | 'image' | 'qr') => {
if (disabled) {
return;
}
@@ -119,7 +130,7 @@ export const SignaturePad = ({
setTab(value);
match(value)
.with('draw', () => {
.with(P.union('draw', 'qr'), () => {
onDrawSignatureChange(drawSignature);
})
.with('text', () => {
@@ -131,7 +142,7 @@ export const SignaturePad = ({
.exhaustive();
};
if (!drawSignatureEnabled && !typedSignatureEnabled && !uploadSignatureEnabled) {
if (!drawSignatureEnabled && !typedSignatureEnabled && !uploadSignatureEnabled && !qrSignatureEnabled) {
return null;
}
@@ -142,7 +153,7 @@ export const SignaturePad = ({
'pointer-events-none': disabled,
})}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
onValueChange={(value) => onTabChange(value as 'draw' | 'text' | 'image')}
onValueChange={(value) => onTabChange(value as 'draw' | 'text' | 'image' | 'qr')}
>
<TabsList>
{drawSignatureEnabled && (
@@ -152,6 +163,13 @@ export const SignaturePad = ({
</TabsTrigger>
)}
{qrSignatureEnabled && (
<TabsTrigger value="qr" className="max-sm:hidden">
<SmartphoneIcon className="mr-2 size-4" />
<Trans context="Sign using a mobile phone">Mobile</Trans>
</TabsTrigger>
)}
{typedSignatureEnabled && (
<TabsTrigger value="text">
<KeyboardIcon className="mr-2 size-4" />
@@ -174,6 +192,19 @@ export const SignaturePad = ({
<SignaturePadDraw className="h-full w-full" onChange={onDrawSignatureChange} value={drawSignature} />
</TabsContent>
<TabsContent
value="qr"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-muted/25 text-center"
>
<SignaturePadQr
value={drawSignature}
onChange={onDrawSignatureChange}
session={qrSession}
context={qrSignatureContext}
onSessionChange={setQrSession}
/>
</TabsContent>
<TabsContent
value="text"
className="relative flex aspect-signature-pad items-center justify-center rounded-md border border-border bg-muted/25 text-center"