/** * This is an internal test page for the embedding system. * * We use this to test embeds for E2E testing. * * No translations required. */ import { useCallback, useEffect, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router'; export const loader = () => { if (process.env.NODE_ENV !== 'development') { throw new Error('This page is only available in development mode.'); } }; /** * Playground embed test page. * * Simulates an embedding parent that renders the V2 authoring iframe * with configurable features, externalId, and mode. * * Navigate to /embed/playground to use. */ export default function EmbedPlaygroundPage() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const [token, setToken] = useState(() => searchParams.get('token') || ''); const [externalId, setExternalId] = useState(() => searchParams.get('externalId') || ''); const [mode, setMode] = useState<'create' | 'edit'>( () => (searchParams.get('mode') as 'create' | 'edit') || 'create', ); const [envelopeId, setEnvelopeId] = useState(() => searchParams.get('envelopeId') || ''); const [envelopeType, setEnvelopeType] = useState<'DOCUMENT' | 'TEMPLATE'>( () => (searchParams.get('envelopeType') as 'DOCUMENT' | 'TEMPLATE') || 'DOCUMENT', ); const [folderId, setFolderId] = useState(() => searchParams.get('folderId') || ''); const [language, setLanguage] = useState(() => searchParams.get('language') || ''); // Auto-launch if query params are present on mount const [iframeSrc, setIframeSrc] = useState(null); const [iframeKey, setIframeKey] = useState(0); const [messages, setMessages] = useState([]); // Feature flags state -- grouped by section const [generalFeatures, setGeneralFeatures] = useState({ allowConfigureEnvelopeTitle: true, allowUploadAndRecipientStep: true, allowAddFieldsStep: true, allowPreviewStep: true, minimizeLeftSidebar: true, }); const [settingsFeatures, setSettingsFeatures] = useState({ allowConfigureSignatureTypes: true, allowConfigureLanguage: true, allowConfigureDateFormat: true, allowConfigureTimezone: true, allowConfigureRedirectUrl: true, allowConfigureDistribution: true, allowConfigureExpirationPeriod: true, allowConfigureEmailSender: true, allowConfigureEmailReplyTo: true, }); const [actionsFeatures, setActionsFeatures] = useState({ allowAttachments: true, allowDistributing: false, allowDirectLink: false, allowDuplication: false, allowSaveAsTemplate: false, allowDownloadPDF: false, allowDeletion: false, }); const [envelopeItemsFeatures, setEnvelopeItemsFeatures] = useState({ allowConfigureTitle: true, allowConfigureOrder: true, allowUpload: true, allowDelete: true, allowReplace: true, }); const [recipientsFeatures, setRecipientsFeatures] = useState({ allowAIDetection: true, allowConfigureSigningOrder: true, allowConfigureDictateNextSigner: true, allowApproverRole: true, allowViewerRole: true, allowCCerRole: true, allowAssistantRole: true, }); const [fieldsFeatures, setFieldsFeatures] = useState({ allowAIDetection: true, }); // CSS theming state const [darkModeDisabled, setDarkModeDisabled] = useState(false); const [rawCss, setRawCss] = useState(''); const [cssVars, setCssVars] = useState>({ background: '', envelopeEditorBackground: '', foreground: '', muted: '', mutedForeground: '', popover: '', popoverForeground: '', card: '', cardBorder: '', cardBorderTint: '', cardForeground: '', fieldCard: '', fieldCardBorder: '', fieldCardForeground: '', widget: '', widgetForeground: '', border: '', input: '', primary: '', primaryForeground: '', secondary: '', secondaryForeground: '', accent: '', accentForeground: '', destructive: '', destructiveForeground: '', ring: '', radius: '', warning: '', }); const [isResolvingToken, setIsResolvingToken] = useState(false); const [tokenError, setTokenError] = useState(null); const messagesEndRef = useRef(null); const hasAutoLaunched = useRef(false); /** * If the token starts with "api_", exchange it for a presign token * via the embedding presign endpoint. Otherwise return as-is. */ const resolveToken = async (inputToken: string): Promise => { if (!inputToken.startsWith('api_')) { return inputToken; } const response = await fetch('/api/v2/embedding/create-presign-token', { method: 'POST', headers: { Authorization: `Bearer ${inputToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({}), }); if (!response.ok) { const text = await response.text(); throw new Error(`Failed to exchange API token (${response.status}): ${text}`); } const data = await response.json(); const presignToken = data?.token; if (!presignToken || typeof presignToken !== 'string') { throw new Error(`Unexpected response shape: ${JSON.stringify(data)}`); } return presignToken; }; useEffect(() => { const handleMessage = (event: MessageEvent) => { const timestamp = new Date().toISOString().slice(11, 19); setMessages((prev) => [...prev, `[${timestamp}] ${JSON.stringify(event.data, null, 2)}`]); }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Auto-launch on mount if token is present in query params useEffect(() => { if (hasAutoLaunched.current) { return; } const initialToken = searchParams.get('token'); if (initialToken) { hasAutoLaunched.current = true; void launchEmbed(initialToken); } }, []); const updateQueryParams = (params: { token: string; externalId: string; mode: string; envelopeId: string; envelopeType: string; folderId: string; language: string; }) => { const newParams = new URLSearchParams(); if (params.token) { newParams.set('token', params.token); } if (params.externalId) { newParams.set('externalId', params.externalId); } if (params.mode && params.mode !== 'create') { newParams.set('mode', params.mode); } if (params.envelopeId) { newParams.set('envelopeId', params.envelopeId); } if (params.envelopeType && params.envelopeType !== 'DOCUMENT') { newParams.set('envelopeType', params.envelopeType); } if (params.folderId) { newParams.set('folderId', params.folderId); } if (params.language) { newParams.set('language', params.language); } const qs = newParams.toString(); void navigate(qs ? `?${qs}` : '.', { replace: true }); }; const launchEmbed = async (overrideToken?: string) => { const inputToken = overrideToken ?? token; if (!inputToken) { return; } setTokenError(null); setIsResolvingToken(true); let presignToken: string; try { presignToken = await resolveToken(inputToken); } catch (err) { setTokenError(err instanceof Error ? err.message : String(err)); setIsResolvingToken(false); return; } setIsResolvingToken(false); // Filter out empty cssVars entries const filteredCssVars: Record = {}; for (const [key, value] of Object.entries(cssVars)) { if (value) { filteredCssVars[key] = value; } } const hashData = { externalId: externalId || undefined, type: mode === 'create' ? envelopeType : undefined, folderId: mode === 'create' && folderId ? folderId : undefined, language: language || undefined, darkModeDisabled: darkModeDisabled || undefined, css: rawCss || undefined, cssVars: Object.keys(filteredCssVars).length > 0 ? filteredCssVars : undefined, features: { general: generalFeatures, settings: settingsFeatures, actions: actionsFeatures, envelopeItems: envelopeItemsFeatures, recipients: recipientsFeatures, fields: fieldsFeatures, }, }; const hash = btoa(encodeURIComponent(JSON.stringify(hashData))); const basePath = mode === 'create' ? '/embed/v2/authoring/envelope/create' : `/embed/v2/authoring/envelope/edit/${envelopeId}`; const buildIframeSrc = (path: string, tokenValue: string, hashValue: string): string => { // Ensure the token is treated strictly as a query parameter value. const encodedToken = encodeURIComponent(tokenValue); return `${path}?token=${encodedToken}#${hashValue}`; }; setIframeSrc(buildIframeSrc(basePath, presignToken, hash)); setIframeKey((prev) => prev + 1); updateQueryParams({ token: inputToken, externalId, mode, envelopeId, envelopeType, folderId, language, }); }; const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); void launchEmbed(); }, [ token, externalId, mode, envelopeId, envelopeType, folderId, language, generalFeatures, settingsFeatures, actionsFeatures, envelopeItemsFeatures, recipientsFeatures, fieldsFeatures, darkModeDisabled, rawCss, cssVars, ], ); const handleClear = () => { setToken(''); setExternalId(''); setMode('create'); setEnvelopeId(''); setEnvelopeType('DOCUMENT'); setFolderId(''); setLanguage(''); setIframeSrc(null); setMessages([]); setTokenError(null); setDarkModeDisabled(false); setRawCss(''); setCssVars((prev) => { const cleared: Record = {}; for (const key of Object.keys(prev)) { cleared[key] = ''; } return cleared; }); void navigate('.', { replace: true }); }; const renderCheckboxGroup = >( label: string, state: T, setState: React.Dispatch>, ) => (
{label} {Object.entries(state).map(([key, value]) => ( ))}
); return (
{/* Left panel: controls */}

Embed Test

setToken(e.target.value)} style={{ width: '100%', padding: '4px', fontSize: '12px' }} placeholder="api_... or presign token" required /> {tokenError &&
{tokenError}
}
setExternalId(e.target.value)} style={{ width: '100%', padding: '4px', fontSize: '12px' }} placeholder="your-correlation-id" />
{mode === 'create' && ( <>
setFolderId(e.target.value)} style={{ width: '100%', padding: '4px', fontSize: '12px' }} placeholder="folder cuid" />
)} {mode === 'edit' && (
setEnvelopeId(e.target.value)} style={{ width: '100%', padding: '4px', fontSize: '12px' }} placeholder="envelope_..." required />
)}

Feature Flags

{renderCheckboxGroup('General', generalFeatures, setGeneralFeatures)} {renderCheckboxGroup('Settings', settingsFeatures, setSettingsFeatures)} {renderCheckboxGroup('Actions', actionsFeatures, setActionsFeatures)} {renderCheckboxGroup('Envelope Items', envelopeItemsFeatures, setEnvelopeItemsFeatures)} {renderCheckboxGroup('Recipients', recipientsFeatures, setRecipientsFeatures)} {renderCheckboxGroup('Fields', fieldsFeatures, setFieldsFeatures)}

CSS Theming

CSS Variables
{Object.entries(cssVars).map(([key, value]) => (
{key !== 'radius' && ( setCssVars((prev) => ({ ...prev, [key]: e.target.value }))} style={{ width: '24px', height: '20px', padding: 0, border: 'none' }} /> )} setCssVars((prev) => ({ ...prev, [key]: e.target.value }))} style={{ flex: 1, padding: '2px 4px', fontSize: '11px' }} placeholder={key === 'radius' ? '0.5rem' : '#hex or color'} /> {value && ( )}
))}
Raw CSS