Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions demo/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});

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

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

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

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

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

const annotatedFields = normalizeFields(fields);
const annotatedFields = normalizeFields(fields, signatureMode);

const { base64, contentType } = await annotateDocument({
documentUrl: document.url,
Expand Down Expand Up @@ -129,6 +130,7 @@ app.post('/v1/sign', async (req, res) => {
certificate,
metadata,
fileName = 'signed-document.pdf',
signatureMode = 'sign',
} = req.body || {};

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

const annotatedFields = normalizeFields({
document: documentFields,
signer: signerFields,
});
const annotatedFields = normalizeFields(
{
document: documentFields,
signer: signerFields,
},
signatureMode,
);

const { base64: annotatedBase64 } = await annotateDocument({
documentUrl: document.url,
Expand Down
56 changes: 56 additions & 0 deletions demo/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -484,4 +484,60 @@ header {

.superdoc-esign-actions {
gap: 12px;
}

/* Main layout container */
.main-layout-container {
display: flex;
gap: 24px;
}

/* Main content area */
.main-content-area {
flex: 1;
min-width: 0;
}

/* Right sidebar */
.document-fields-sidebar {
width: 280px;
flex-shrink: 0;
padding: 16px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
align-self: flex-start;
}

.document-fields-sidebar h3 {
margin: 0 0 16px;
font-size: 14px;
font-weight: 600;
color: #374151;
}

.document-fields-list {
display: flex;
flex-direction: column;
gap: 12px;
}

.document-field {
/* Individual field styles handled inline for now */
}

/* Responsive layout - stack vertically on small screens */
@media (max-width: 768px) {
.main-layout-container {
flex-direction: column;
}

.document-fields-sidebar {
width: 100%;
order: 2; /* Move sidebar below the main content */
}

.main-content-area {
order: 1;
}
}
109 changes: 66 additions & 43 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useRef } from 'react';
import SuperDocESign from '@superdoc-dev/esign';
import SuperDocESign, { textToImageDataUrl } from '@superdoc-dev/esign';
import type {
SubmitData,
SigningState,
Expand All @@ -16,6 +16,54 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
const documentSource =
'https://storage.googleapis.com/public_static_hosting/public_demo_docs/service_agreement_updated.docx';

const signerFieldsConfig = [
{
id: '789012',
type: 'signature' as const,
label: 'Your Signature',
validation: { required: true },
component: CustomSignature,
},
{
id: 'terms',
type: 'checkbox' as const,
label: 'I accept the terms and conditions',
validation: { required: true },
},
{
id: 'email',
type: 'checkbox' as const,
label: 'Send me a copy of the agreement',
validation: { required: false },
},
];

const signatureFieldIds = new Set(
signerFieldsConfig.filter((field) => field.type === 'signature').map((field) => field.id),
);

const toSignatureImageValue = (value: SubmitData['signerFields'][number]['value']) => {
if (value === null || value === undefined) return null;
if (typeof value === 'string' && value.startsWith('data:image/')) return value;
return textToImageDataUrl(String(value));
};

const mapSignerFieldsWithType = (
fields: Array<{ id: string; value: SubmitData['signerFields'][number]['value'] }>,
signatureType: 'signature' | 'image',
) =>
fields.map((field) => {
if (!signatureFieldIds.has(field.id)) {
return field;
}

return {
...field,
type: signatureType,
value: toSignatureImageValue(field.value),
};
});

// Helper to download a response blob as a file
const downloadBlob = async (response: Response, fileName: string) => {
const blob = await response.blob();
Expand Down Expand Up @@ -93,13 +141,15 @@ export function App() {
console.log('Submit data:', data);

try {
const signerFields = mapSignerFieldsWithType(data.signerFields, 'signature');

const response = await fetch(`${API_BASE_URL}/v1/sign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
document: { url: documentSource },
documentFields: data.documentFields,
signerFields: data.signerFields,
signerFields,
auditTrail: data.auditTrail,
eventId: data.eventId,
certificate: { enable: true },
Expand All @@ -108,6 +158,7 @@ export function App() {
plan: documentFields['456789'],
},
fileName: `signed_agreement_${data.eventId}.pdf`,
signatureMode: 'sign',
}),
});

Expand All @@ -134,13 +185,19 @@ export function App() {
return;
}

const signerFields = mapSignerFieldsWithType(data.fields.signer, 'image');

const response = await fetch(`${API_BASE_URL}/v1/download`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
document: { url: data.documentSource },
fields: data.fields,
fields: {
...data.fields,
signer: signerFields,
},
fileName: data.fileName,
signatureMode: 'annotate',
}),
});

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

<div style={{ display: 'flex', gap: '24px' }}>
<div className="main-layout-container">
{/* Main content */}
<div style={{ flex: 1, minWidth: 0 }}>
<div className="main-content-area">
<SuperDocESign
ref={esignRef}
eventId={eventId}
Expand All @@ -272,27 +329,7 @@ export function App() {
value: documentFields[f.id],
type: f.type,
})),
signer: [
{
id: '789012',
type: 'signature',
label: 'Your Signature',
validation: { required: true },
component: CustomSignature,
},
{
id: 'terms',
type: 'checkbox',
label: 'I accept the terms and conditions',
validation: { required: true },
},
{
id: 'email',
type: 'checkbox',
label: 'Send me a copy of the agreement',
validation: { required: false },
},
],
signer: signerFieldsConfig,
}}
download={{ label: 'Download PDF' }}
onSubmit={handleSubmit}
Expand Down Expand Up @@ -335,23 +372,9 @@ export function App() {
</div>

{/* Right Sidebar - Document Fields */}
<div
style={{
width: '280px',
flexShrink: 0,
padding: '16px',
background: '#f9fafb',
border: '1px solid #e5e7eb',
borderRadius: '8px',
alignSelf: 'flex-start',
}}
>
<h3
style={{ margin: '0 0 16px', fontSize: '14px', fontWeight: 600, color: '#374151' }}
>
Document Fields
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div className="document-fields-sidebar">
<h3>Document Fields</h3>
<div className="document-fields-list">
{documentFieldsConfig.map((field) => (
<div key={field.id}>
<label
Expand Down
Loading
Loading