Installation
No additional dependencies are required beyond React. For TypeScript support, ensure you have type definitions:npm install react react-dom
npm install -D @types/react @types/react-dom
Basic Signing Component
DocumensoEmbed Component
import { useEffect, useState, useCallback } from 'react';
export interface DocumensoEmbedProps {
/** Recipient signing token */
token: string;
/** Pre-fill recipient name */
name?: string;
/** Lock name field (prevent editing) */
lockName?: boolean;
/** Pre-fill recipient email */
email?: string;
/** Lock email field (prevent editing) */
lockEmail?: boolean;
/** Allow recipient to reject document */
allowDocumentRejection?: boolean;
/** Show fields completed by other recipients */
showOtherRecipientsCompletedFields?: boolean;
/** Disable dark mode */
darkModeDisabled?: boolean;
/** Custom CSS (Enterprise only) */
css?: string;
/** CSS variables for theming (Enterprise only) */
cssVars?: Record<string, string>;
/** Documenso instance URL */
baseUrl?: string;
/** Loading state callback */
onReady?: () => void;
/** Document completed callback */
onComplete?: (data: {
token: string;
documentId: number;
envelopeId: string;
recipientId: number;
}) => void;
/** Document rejected callback */
onReject?: (data: {
token: string;
documentId: number;
envelopeId: string;
recipientId: number;
reason?: string;
}) => void;
/** Error callback */
onError?: () => void;
/** Field signed callback */
onFieldSigned?: (data: {
fieldId?: number;
value?: string;
isBase64?: boolean;
}) => void;
/** Field unsigned callback */
onFieldUnsigned?: (data: { fieldId?: number }) => void;
/** iframe className */
className?: string;
/** iframe style */
style?: React.CSSProperties;
}
export function DocumensoEmbed({
token,
name,
lockName = false,
email,
lockEmail = false,
allowDocumentRejection = false,
showOtherRecipientsCompletedFields = false,
darkModeDisabled = false,
css,
cssVars,
baseUrl = 'https://app.documenso.com',
onReady,
onComplete,
onReject,
onError,
onFieldSigned,
onFieldUnsigned,
className = 'w-full h-screen border-0',
style
}: DocumensoEmbedProps) {
const [embedUrl, setEmbedUrl] = useState<string>('');
useEffect(() => {
// Build embed data
const embedData: Record<string, any> = {
darkModeDisabled
};
if (name) embedData.name = name;
if (lockName) embedData.lockName = lockName;
if (email) embedData.email = email;
if (lockEmail) embedData.lockEmail = lockEmail;
if (allowDocumentRejection) embedData.allowDocumentRejection = allowDocumentRejection;
if (showOtherRecipientsCompletedFields) {
embedData.showOtherRecipientsCompletedFields = showOtherRecipientsCompletedFields;
}
if (css) embedData.css = css;
if (cssVars) embedData.cssVars = cssVars;
// Encode as URL hash
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
const url = `${baseUrl}/embed/sign/${token}#${hash}`;
setEmbedUrl(url);
}, [
token,
name,
lockName,
email,
lockEmail,
allowDocumentRejection,
showOtherRecipientsCompletedFields,
darkModeDisabled,
css,
cssVars,
baseUrl
]);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Verify origin for security
const allowedOrigin = new URL(baseUrl).origin;
if (event.origin !== allowedOrigin) {
return;
}
switch (event.data.action) {
case 'document-ready':
onReady?.();
break;
case 'document-completed':
onComplete?.(event.data.data);
break;
case 'document-rejected':
onReject?.(event.data.data);
break;
case 'document-error':
onError?.();
break;
case 'field-signed':
onFieldSigned?.(event.data.data);
break;
case 'field-unsigned':
onFieldUnsigned?.(event.data.data);
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [baseUrl, onReady, onComplete, onReject, onError, onFieldSigned, onFieldUnsigned]);
return (
<iframe
src={embedUrl}
className={className}
style={style}
allow="clipboard-write"
title="Documenso Document Signing"
/>
);
}
Usage Examples
Basic Usage
import { DocumensoEmbed } from './components/DocumensoEmbed';
function SigningPage() {
return (
<div className="container mx-auto p-4">
<h1>Sign Your Contract</h1>
<DocumensoEmbed
token="recipient-token-xyz"
onComplete={(data) => {
console.log('Document signed!', data);
// Redirect to success page
window.location.href = '/success';
}}
/>
</div>
);
}
Pre-filled and Locked Fields
function CustomerSigningPage({ customerEmail, customerName }: {
customerEmail: string;
customerName: string;
}) {
return (
<DocumensoEmbed
token="token-123"
email={customerEmail}
lockEmail={true} // Prevent customer from changing email
name={customerName}
lockName={false} // Allow name editing
allowDocumentRejection={true}
onComplete={async (data) => {
// Save to your database
await fetch('/api/documents/completed', {
method: 'POST',
body: JSON.stringify(data)
});
}}
/>
);
}
Custom Styling
function BrandedSigningPage({ token }: { token: string }) {
return (
<DocumensoEmbed
token={token}
cssVars={{
primary: '#3b82f6',
background: '#ffffff',
foreground: '#0f172a',
radius: '0.75rem'
}}
css={`
.embed--Root {
font-family: 'Inter', sans-serif;
}
button {
font-weight: 600;
}
`}
/>
);
}
Advanced Component with Loading State
import { useState } from 'react';
import { DocumensoEmbed } from './DocumensoEmbed';
export function SigningPageWithLoader({ token }: { token: string }) {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
return (
<div className="relative w-full h-screen">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-50 z-10">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4" />
<p className="text-gray-600">Loading document...</p>
</div>
</div>
)}
{error && (
<div className="absolute inset-0 flex items-center justify-center bg-red-50 z-10">
<div className="text-center">
<p className="text-red-600 text-lg font-semibold">Error loading document</p>
<p className="text-red-500 mt-2">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded"
>
Retry
</button>
</div>
</div>
)}
<DocumensoEmbed
token={token}
onReady={() => setIsLoading(false)}
onError={() => {
setIsLoading(false);
setError('Failed to load the document. Please try again.');
}}
onComplete={(data) => {
console.log('Completed:', data);
window.location.href = '/success';
}}
/>
</div>
);
}
Direct Template Component
import { useEffect, useState } from 'react';
export interface DirectTemplateEmbedProps {
/** Direct template token */
directToken: string;
/** Pre-fill recipient email */
email?: string;
/** Lock email field */
lockEmail?: boolean;
/** Pre-fill recipient name */
name?: string;
/** Lock name field */
lockName?: boolean;
/** External reference ID */
externalId?: string;
/** CSS variables */
cssVars?: Record<string, string>;
/** Custom CSS */
css?: string;
/** Base URL */
baseUrl?: string;
/** Callbacks */
onReady?: () => void;
onComplete?: (data: { token: string; documentId: number; recipientId: number }) => void;
onError?: (error: string) => void;
/** Styling */
className?: string;
}
export function DirectTemplateEmbed({
directToken,
email,
lockEmail = false,
name,
lockName = false,
externalId,
cssVars,
css,
baseUrl = 'https://app.documenso.com',
onReady,
onComplete,
onError,
className = 'w-full h-screen border-0'
}: DirectTemplateEmbedProps) {
const [embedUrl, setEmbedUrl] = useState<string>('');
useEffect(() => {
const embedData: Record<string, any> = {};
if (email) embedData.email = email;
if (lockEmail) embedData.lockEmail = lockEmail;
if (name) embedData.name = name;
if (lockName) embedData.lockName = lockName;
if (css) embedData.css = css;
if (cssVars) embedData.cssVars = cssVars;
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
const queryParams = externalId ? `?externalId=${encodeURIComponent(externalId)}` : '';
const url = `${baseUrl}/d/${directToken}${queryParams}#${hash}`;
setEmbedUrl(url);
}, [directToken, email, lockEmail, name, lockName, externalId, css, cssVars, baseUrl]);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const allowedOrigin = new URL(baseUrl).origin;
if (event.origin !== allowedOrigin) return;
switch (event.data.action) {
case 'document-ready':
onReady?.();
break;
case 'document-completed':
onComplete?.(event.data.data);
break;
case 'document-error':
onError?.(event.data.data);
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [baseUrl, onReady, onComplete, onError]);
return (
<iframe
src={embedUrl}
className={className}
allow="clipboard-write"
title="Sign Document"
/>
);
}
Direct Template Usage
function PublicSigningForm() {
return (
<DirectTemplateEmbed
directToken="direct-template-xyz"
externalId="order-12345"
onComplete={(data) => {
// Track completion
analytics.track('Document Signed', data);
// Show success
toast.success('Document signed successfully!');
}}
/>
);
}
Custom Hook
import { useState, useEffect, useCallback } from 'react';
interface UseDocumensoEmbedOptions {
token: string;
baseUrl?: string;
}
export function useDocumensoEmbed({ token, baseUrl = 'https://app.documenso.com' }: UseDocumensoEmbedOptions) {
const [isReady, setIsReady] = useState(false);
const [isCompleted, setIsCompleted] = useState(false);
const [error, setError] = useState<string | null>(null);
const [completionData, setCompletionData] = useState<any>(null);
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const allowedOrigin = new URL(baseUrl).origin;
if (event.origin !== allowedOrigin) return;
switch (event.data.action) {
case 'document-ready':
setIsReady(true);
break;
case 'document-completed':
setIsCompleted(true);
setCompletionData(event.data.data);
break;
case 'document-error':
setError('An error occurred while loading the document');
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [baseUrl]);
const reset = useCallback(() => {
setIsReady(false);
setIsCompleted(false);
setError(null);
setCompletionData(null);
}, []);
return {
isReady,
isCompleted,
error,
completionData,
reset
};
}
// Usage
function SigningPage({ token }: { token: string }) {
const { isReady, isCompleted, error, completionData } = useDocumensoEmbed({ token });
if (error) {
return <div>Error: {error}</div>;
}
if (isCompleted) {
return (
<div>
<h2>Document Signed!</h2>
<p>Document ID: {completionData.documentId}</p>
</div>
);
}
return (
<div>
{!isReady && <div>Loading...</div>}
<DocumensoEmbed token={token} />
</div>
);
}
Next.js Integration
App Router (Next.js 13+)
'use client';
import { DocumensoEmbed } from '@/components/DocumensoEmbed';
import { useRouter } from 'next/navigation';
export default function SignDocumentPage({ params }: { params: { token: string } }) {
const router = useRouter();
return (
<div className="h-screen">
<DocumensoEmbed
token={params.token}
onComplete={async (data) => {
// Save to database via API route
await fetch('/api/documents/complete', {
method: 'POST',
body: JSON.stringify(data)
});
// Redirect to success page
router.push('/documents/success');
}}
onError={() => {
router.push('/documents/error');
}}
/>
</div>
);
}
Pages Router
import { useRouter } from 'next/router';
import { DocumensoEmbed } from '@/components/DocumensoEmbed';
export default function SignPage() {
const router = useRouter();
const { token } = router.query;
if (!token || typeof token !== 'string') {
return <div>Invalid token</div>;
}
return (
<DocumensoEmbed
token={token}
onComplete={() => router.push('/success')}
/>
);
}
TypeScript Types
// types/documenso.ts
export interface EmbedEvent {
action: 'document-ready' | 'document-completed' | 'document-rejected' | 'document-error' | 'field-signed' | 'field-unsigned';
data: any;
}
export interface DocumentCompletedData {
token: string;
documentId: number;
envelopeId: string;
recipientId: number;
}
export interface DocumentRejectedData extends DocumentCompletedData {
reason?: string;
}
export interface FieldSignedData {
fieldId?: number;
value?: string;
isBase64?: boolean;
}
export interface FieldUnsignedData {
fieldId?: number;
}
export interface CssVariables {
background?: string;
foreground?: string;
primary?: string;
primaryForeground?: string;
secondary?: string;
secondaryForeground?: string;
border?: string;
radius?: string;
// ... add more as needed
}
export interface EmbedData {
email?: string;
lockEmail?: boolean;
name?: string;
lockName?: boolean;
allowDocumentRejection?: boolean;
showOtherRecipientsCompletedFields?: boolean;
darkModeDisabled?: boolean;
css?: string;
cssVars?: CssVariables;
}
Testing
import { render, screen } from '@testing-library/react';
import { DocumensoEmbed } from './DocumensoEmbed';
describe('DocumensoEmbed', () => {
it('renders iframe with correct src', () => {
render(<DocumensoEmbed token="test-token" />);
const iframe = screen.getByTitle('Documenso Document Signing');
expect(iframe).toBeInTheDocument();
expect(iframe).toHaveAttribute('src');
});
it('calls onReady when document is ready', () => {
const onReady = jest.fn();
render(<DocumensoEmbed token="test-token" onReady={onReady} />);
// Simulate postMessage from iframe
window.postMessage({ action: 'document-ready', data: null }, window.location.origin);
expect(onReady).toHaveBeenCalled();
});
});
Best Practices
- Always validate origin in message event listeners
- Use TypeScript for type safety
- Handle errors gracefully with user-friendly messages
- Show loading states for better UX
- Clean up event listeners in useEffect cleanup
- Memoize URLs to avoid unnecessary re-renders
- Test iframe integration in different browsers
Next Steps
Angular SDK
Build Angular wrapper components
Vue SDK
Build Vue wrapper components
