Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit dd20f37

Browse files
improved signing experience and fixed annotations logic (#14)
* feat: enhance signature handling and API integration - Updated the `normalizeFields` function to accept a `signatureMode` parameter for better handling of signature types. - Introduced a new utility function `textToImageDataUrl` to convert typed signatures into PNG data URLs. - Refactored the App component to utilize the new signature handling logic, including mapping signer fields based on type. - Added unit tests for the `textToImageDataUrl` function to ensure correct functionality. - Improved the demo to include a configuration for signer fields and updated API calls for signing and downloading documents. * feat: resize signature pad and trim signature whitespaces * feat: refactor layout with CSS classes for main content and sidebar
1 parent e151289 commit dd20f37

7 files changed

Lines changed: 292 additions & 82 deletions

File tree

demo/server/server.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ app.get('/health', (req, res) => {
2929
res.json({ status: 'ok' });
3030
});
3131

32-
const normalizeFields = (fieldsPayload = {}) => {
32+
const normalizeFields = (fieldsPayload = {}, signatureMode = 'annotate') => {
3333
const documentFields = Array.isArray(fieldsPayload.document) ? fieldsPayload.document : [];
3434
const signerFields = Array.isArray(fieldsPayload.signer) ? fieldsPayload.signer : [];
3535

@@ -38,8 +38,8 @@ const normalizeFields = (fieldsPayload = {}) => {
3838
.map((field) => {
3939
const isSignatureField = field.id === SIGNATURE_FIELD_ID;
4040
const value = field.value ?? '';
41-
const isDrawnSignature = typeof value === 'string' && value.startsWith('data:image/');
42-
const type = isSignatureField && isDrawnSignature ? 'signature' : 'text';
41+
const signatureType = signatureMode === 'sign' ? 'signature' : 'image';
42+
const type = isSignatureField ? signatureType : 'text';
4343

4444
const normalized = { id: field.id, value, type };
4545
if (type === 'signature') {
@@ -85,7 +85,8 @@ const sendPdfBuffer = (res, base64, fileName, contentType = 'application/pdf') =
8585

8686
app.post('/v1/download', async (req, res) => {
8787
try {
88-
const { document, fields = {}, fileName = 'document.pdf' } = req.body || {};
88+
const { document, fields = {}, fileName = 'document.pdf', signatureMode = 'annotate' } =
89+
req.body || {};
8990

9091
if (!SUPERDOC_SERVICES_API_KEY) {
9192
return res.status(500).json({ error: 'Missing SUPERDOC_SERVICES_API_KEY on the server' });
@@ -95,7 +96,7 @@ app.post('/v1/download', async (req, res) => {
9596
return res.status(400).json({ error: 'document.url is required' });
9697
}
9798

98-
const annotatedFields = normalizeFields(fields);
99+
const annotatedFields = normalizeFields(fields, signatureMode);
99100

100101
const { base64, contentType } = await annotateDocument({
101102
documentUrl: document.url,
@@ -129,6 +130,7 @@ app.post('/v1/sign', async (req, res) => {
129130
certificate,
130131
metadata,
131132
fileName = 'signed-document.pdf',
133+
signatureMode = 'sign',
132134
} = req.body || {};
133135

134136
if (!SUPERDOC_SERVICES_API_KEY) {
@@ -139,10 +141,13 @@ app.post('/v1/sign', async (req, res) => {
139141
return res.status(400).json({ error: 'document.url is required' });
140142
}
141143

142-
const annotatedFields = normalizeFields({
143-
document: documentFields,
144-
signer: signerFields,
145-
});
144+
const annotatedFields = normalizeFields(
145+
{
146+
document: documentFields,
147+
signer: signerFields,
148+
},
149+
signatureMode,
150+
);
146151

147152
const { base64: annotatedBase64 } = await annotateDocument({
148153
documentUrl: document.url,

demo/src/App.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,4 +484,60 @@ header {
484484

485485
.superdoc-esign-actions {
486486
gap: 12px;
487+
}
488+
489+
/* Main layout container */
490+
.main-layout-container {
491+
display: flex;
492+
gap: 24px;
493+
}
494+
495+
/* Main content area */
496+
.main-content-area {
497+
flex: 1;
498+
min-width: 0;
499+
}
500+
501+
/* Right sidebar */
502+
.document-fields-sidebar {
503+
width: 280px;
504+
flex-shrink: 0;
505+
padding: 16px;
506+
background: #f9fafb;
507+
border: 1px solid #e5e7eb;
508+
border-radius: 8px;
509+
align-self: flex-start;
510+
}
511+
512+
.document-fields-sidebar h3 {
513+
margin: 0 0 16px;
514+
font-size: 14px;
515+
font-weight: 600;
516+
color: #374151;
517+
}
518+
519+
.document-fields-list {
520+
display: flex;
521+
flex-direction: column;
522+
gap: 12px;
523+
}
524+
525+
.document-field {
526+
/* Individual field styles handled inline for now */
527+
}
528+
529+
/* Responsive layout - stack vertically on small screens */
530+
@media (max-width: 768px) {
531+
.main-layout-container {
532+
flex-direction: column;
533+
}
534+
535+
.document-fields-sidebar {
536+
width: 100%;
537+
order: 2; /* Move sidebar below the main content */
538+
}
539+
540+
.main-content-area {
541+
order: 1;
542+
}
487543
}

demo/src/App.tsx

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useRef } from 'react';
2-
import SuperDocESign from '@superdoc-dev/esign';
2+
import SuperDocESign, { textToImageDataUrl } from '@superdoc-dev/esign';
33
import type {
44
SubmitData,
55
SigningState,
@@ -16,6 +16,54 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
1616
const documentSource =
1717
'https://storage.googleapis.com/public_static_hosting/public_demo_docs/service_agreement_updated.docx';
1818

19+
const signerFieldsConfig = [
20+
{
21+
id: '789012',
22+
type: 'signature' as const,
23+
label: 'Your Signature',
24+
validation: { required: true },
25+
component: CustomSignature,
26+
},
27+
{
28+
id: 'terms',
29+
type: 'checkbox' as const,
30+
label: 'I accept the terms and conditions',
31+
validation: { required: true },
32+
},
33+
{
34+
id: 'email',
35+
type: 'checkbox' as const,
36+
label: 'Send me a copy of the agreement',
37+
validation: { required: false },
38+
},
39+
];
40+
41+
const signatureFieldIds = new Set(
42+
signerFieldsConfig.filter((field) => field.type === 'signature').map((field) => field.id),
43+
);
44+
45+
const toSignatureImageValue = (value: SubmitData['signerFields'][number]['value']) => {
46+
if (value === null || value === undefined) return null;
47+
if (typeof value === 'string' && value.startsWith('data:image/')) return value;
48+
return textToImageDataUrl(String(value));
49+
};
50+
51+
const mapSignerFieldsWithType = (
52+
fields: Array<{ id: string; value: SubmitData['signerFields'][number]['value'] }>,
53+
signatureType: 'signature' | 'image',
54+
) =>
55+
fields.map((field) => {
56+
if (!signatureFieldIds.has(field.id)) {
57+
return field;
58+
}
59+
60+
return {
61+
...field,
62+
type: signatureType,
63+
value: toSignatureImageValue(field.value),
64+
};
65+
});
66+
1967
// Helper to download a response blob as a file
2068
const downloadBlob = async (response: Response, fileName: string) => {
2169
const blob = await response.blob();
@@ -93,13 +141,15 @@ export function App() {
93141
console.log('Submit data:', data);
94142

95143
try {
144+
const signerFields = mapSignerFieldsWithType(data.signerFields, 'signature');
145+
96146
const response = await fetch(`${API_BASE_URL}/v1/sign`, {
97147
method: 'POST',
98148
headers: { 'Content-Type': 'application/json' },
99149
body: JSON.stringify({
100150
document: { url: documentSource },
101151
documentFields: data.documentFields,
102-
signerFields: data.signerFields,
152+
signerFields,
103153
auditTrail: data.auditTrail,
104154
eventId: data.eventId,
105155
certificate: { enable: true },
@@ -108,6 +158,7 @@ export function App() {
108158
plan: documentFields['456789'],
109159
},
110160
fileName: `signed_agreement_${data.eventId}.pdf`,
161+
signatureMode: 'sign',
111162
}),
112163
});
113164

@@ -134,13 +185,19 @@ export function App() {
134185
return;
135186
}
136187

188+
const signerFields = mapSignerFieldsWithType(data.fields.signer, 'image');
189+
137190
const response = await fetch(`${API_BASE_URL}/v1/download`, {
138191
method: 'POST',
139192
headers: { 'Content-Type': 'application/json' },
140193
body: JSON.stringify({
141194
document: { url: data.documentSource },
142-
fields: data.fields,
195+
fields: {
196+
...data.fields,
197+
signer: signerFields,
198+
},
143199
fileName: data.fileName,
200+
signatureMode: 'annotate',
144201
}),
145202
});
146203

@@ -251,9 +308,9 @@ export function App() {
251308
Use the document toolbar to download the current agreement at any time.
252309
</p>
253310

254-
<div style={{ display: 'flex', gap: '24px' }}>
311+
<div className="main-layout-container">
255312
{/* Main content */}
256-
<div style={{ flex: 1, minWidth: 0 }}>
313+
<div className="main-content-area">
257314
<SuperDocESign
258315
ref={esignRef}
259316
eventId={eventId}
@@ -272,27 +329,7 @@ export function App() {
272329
value: documentFields[f.id],
273330
type: f.type,
274331
})),
275-
signer: [
276-
{
277-
id: '789012',
278-
type: 'signature',
279-
label: 'Your Signature',
280-
validation: { required: true },
281-
component: CustomSignature,
282-
},
283-
{
284-
id: 'terms',
285-
type: 'checkbox',
286-
label: 'I accept the terms and conditions',
287-
validation: { required: true },
288-
},
289-
{
290-
id: 'email',
291-
type: 'checkbox',
292-
label: 'Send me a copy of the agreement',
293-
validation: { required: false },
294-
},
295-
],
332+
signer: signerFieldsConfig,
296333
}}
297334
download={{ label: 'Download PDF' }}
298335
onSubmit={handleSubmit}
@@ -335,23 +372,9 @@ export function App() {
335372
</div>
336373

337374
{/* Right Sidebar - Document Fields */}
338-
<div
339-
style={{
340-
width: '280px',
341-
flexShrink: 0,
342-
padding: '16px',
343-
background: '#f9fafb',
344-
border: '1px solid #e5e7eb',
345-
borderRadius: '8px',
346-
alignSelf: 'flex-start',
347-
}}
348-
>
349-
<h3
350-
style={{ margin: '0 0 16px', fontSize: '14px', fontWeight: 600, color: '#374151' }}
351-
>
352-
Document Fields
353-
</h3>
354-
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
375+
<div className="document-fields-sidebar">
376+
<h3>Document Fields</h3>
377+
<div className="document-fields-list">
355378
{documentFieldsConfig.map((field) => (
356379
<div key={field.id}>
357380
<label

0 commit comments

Comments
 (0)