From 73ced91b8165c1f6773d661d64ee30758907c58e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 3 Jan 2026 12:33:07 -0800 Subject: [PATCH 01/17] feat(deployed-form): added deployed form input --- apps/docs/content/docs/en/execution/form.mdx | 357 + apps/docs/content/docs/en/execution/meta.json | 2 +- apps/docs/content/docs/en/triggers/start.mdx | 9 +- apps/sim/app/api/form/[identifier]/route.ts | 418 + apps/sim/app/api/form/manage/[id]/route.ts | 260 + apps/sim/app/api/form/route.ts | 245 + apps/sim/app/api/form/utils.ts | 279 + apps/sim/app/api/form/validate/route.ts | 66 + .../api/workflows/[id]/form/status/route.ts | 48 + .../[identifier]/components/error-state.tsx | 100 + .../[identifier]/components/form-field.tsx | 166 + .../[identifier]/components/form-header.tsx | 83 + .../app/form/[identifier]/components/index.ts | 6 + .../[identifier]/components/loading-state.tsx | 34 + .../[identifier]/components/password-auth.tsx | 107 + .../components/powered-by-sim.tsx | 31 + .../components/thank-you-screen.tsx | 37 + apps/sim/app/form/[identifier]/error.tsx | 50 + .../sim/app/form/[identifier]/form-client.tsx | 360 + apps/sim/app/form/[identifier]/page.tsx | 6 + .../form/components/embed-code-generator.tsx | 61 + .../deploy-modal/components/form/form.tsx | 685 ++ .../components/form/hooks/index.ts | 1 + .../form/hooks/use-form-deployment.ts | 151 + .../form/hooks/use-identifier-validation.ts | 91 + .../components/deploy-modal/deploy-modal.tsx | 64 +- .../components/api-keys/api-keys.tsx | 7 +- .../components/permission-selector.tsx | 4 +- .../emcn/components/button/button.tsx | 2 +- apps/sim/hooks/queries/forms.ts | 238 + apps/sim/lib/core/rate-limiter/types.ts | 1 + apps/sim/lib/core/security/csp.ts | 13 + apps/sim/lib/execution/preprocessing.ts | 2 +- apps/sim/lib/logs/get-trigger-options.ts | 1 + apps/sim/next.config.ts | 37 +- .../db/migrations/0135_violet_scourge.sql | 22 + .../db/migrations/meta/0135_snapshot.json | 8982 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 37 + 39 files changed, 13054 insertions(+), 16 deletions(-) create mode 100644 apps/docs/content/docs/en/execution/form.mdx create mode 100644 apps/sim/app/api/form/[identifier]/route.ts create mode 100644 apps/sim/app/api/form/manage/[id]/route.ts create mode 100644 apps/sim/app/api/form/route.ts create mode 100644 apps/sim/app/api/form/utils.ts create mode 100644 apps/sim/app/api/form/validate/route.ts create mode 100644 apps/sim/app/api/workflows/[id]/form/status/route.ts create mode 100644 apps/sim/app/form/[identifier]/components/error-state.tsx create mode 100644 apps/sim/app/form/[identifier]/components/form-field.tsx create mode 100644 apps/sim/app/form/[identifier]/components/form-header.tsx create mode 100644 apps/sim/app/form/[identifier]/components/index.ts create mode 100644 apps/sim/app/form/[identifier]/components/loading-state.tsx create mode 100644 apps/sim/app/form/[identifier]/components/password-auth.tsx create mode 100644 apps/sim/app/form/[identifier]/components/powered-by-sim.tsx create mode 100644 apps/sim/app/form/[identifier]/components/thank-you-screen.tsx create mode 100644 apps/sim/app/form/[identifier]/error.tsx create mode 100644 apps/sim/app/form/[identifier]/form-client.tsx create mode 100644 apps/sim/app/form/[identifier]/page.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/components/embed-code-generator.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/hooks/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/hooks/use-form-deployment.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/hooks/use-identifier-validation.ts create mode 100644 apps/sim/hooks/queries/forms.ts create mode 100644 packages/db/migrations/0135_violet_scourge.sql create mode 100644 packages/db/migrations/meta/0135_snapshot.json diff --git a/apps/docs/content/docs/en/execution/form.mdx b/apps/docs/content/docs/en/execution/form.mdx new file mode 100644 index 0000000000..c082e41518 --- /dev/null +++ b/apps/docs/content/docs/en/execution/form.mdx @@ -0,0 +1,357 @@ +--- +title: Form Deployment +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' + +Deploy your workflow as an embeddable form that users can fill out on your website or share via link. Form submissions trigger your workflow with the `form` trigger type, and results appear in your execution logs. + +## Overview + +Form deployment turns your workflow's Input Format into a beautiful, responsive form that can be: +- Shared via a direct link (e.g., `https://sim.ai/form/my-survey`) +- Embedded in any website using an iframe +- Customized with your brand colors and messages + +When a user submits the form, it triggers your workflow with the form data, executing all downstream blocks automatically. + + +Forms derive their fields directly from your workflow's Start block Input Format. Each field you define becomes a form input with the appropriate type (text, number, checkbox, etc.). + + +## Creating a Form Deployment + +1. Open your workflow in the editor +2. Click the **Deploy** button in the top-right corner +3. Select the **Form** tab +4. Configure your form settings: + - **Identifier**: A unique URL slug (e.g., `contact-form` creates `https://sim.ai/form/contact-form`) + - **Title**: The form's heading displayed to users + - **Description**: Optional subtitle explaining the form's purpose +5. Click **Launch** to deploy + +## Input Format Mapping + +Your Start block's Input Format fields map to form inputs: + +| Input Format Type | Form Field | +|------------------|------------| +| `string` | Text input (or textarea for fields named "message", "description", etc.) | +| `number` | Number input with validation | +| `boolean` | Toggle switch | +| `object` | JSON editor | +| `array` | JSON array editor | +| `files` | File upload dropzone | + + +Make sure your Start block has at least one Input Format field defined. Forms with no fields will display a message indicating no inputs are configured. + + +## Customization Options + +### Branding + +- **Primary Color**: Hex color for buttons and accents (e.g., `#3972F6`) +- **Logo URL**: Your logo displayed in the form header +- **"Powered by Sim" Branding**: Toggle to show/hide the footer branding + +### Messages + +- **Welcome Message**: Introductory text shown above the form +- **Thank You Title**: Heading displayed after successful submission +- **Thank You Message**: Body text shown after submission + +## Access Control + +Forms support three authentication modes: + +| Mode | Description | +|------|-------------| +| **Public** | Anyone with the link can submit | +| **Password** | Users must enter a password to access the form | +| **Email Whitelist** | Only specified emails or domains can submit | + +For email whitelist, you can specify: +- Exact emails: `user@example.com` +- Domain wildcards: `@example.com` (allows all emails from that domain) + +## Embedding Your Form + +### Direct Link + +Share the form URL directly: + +``` +https://sim.ai/form/your-identifier +``` + +### Iframe Embed + +Embed the form in any webpage: + +```html + +``` + +## Programmatic Form Submission + +You can also submit forms programmatically without using the UI. + + + +```bash +curl -X POST https://sim.ai/api/form/your-identifier \ + -H "Content-Type: application/json" \ + -d '{ + "formData": { + "name": "John Doe", + "email": "john@example.com", + "message": "Hello from the API!" + } + }' +``` + + +```python +import requests + +url = "https://sim.ai/api/form/your-identifier" +payload = { + "formData": { + "name": "John Doe", + "email": "john@example.com", + "message": "Hello from the API!" + } +} + +response = requests.post(url, json=payload) +result = response.json() + +if result.get("success"): + print("Form submitted successfully!") + print(f"Execution ID: {result['data']['executionId']}") +else: + print(f"Error: {result.get('error')}") +``` + + +```typescript +interface FormSubmissionResult { + success: boolean; + data?: { + executionId: string; + thankYouTitle: string; + thankYouMessage: string; + }; + error?: string; +} + +async function submitForm( + identifier: string, + formData: Record +): Promise { + const response = await fetch( + `https://sim.ai/api/form/${identifier}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ formData }), + } + ); + + return response.json(); +} + +// Usage +const result = await submitForm('your-identifier', { + name: 'John Doe', + email: 'john@example.com', + message: 'Hello from TypeScript!', +}); + +if (result.success) { + console.log('Submitted! Execution ID:', result.data?.executionId); +} +``` + + +```go +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type FormPayload struct { + FormData map[string]interface{} `json:"formData"` +} + +type FormResponse struct { + Success bool `json:"success"` + Data struct { + ExecutionID string `json:"executionId"` + ThankYouTitle string `json:"thankYouTitle"` + ThankYouMessage string `json:"thankYouMessage"` + } `json:"data"` + Error string `json:"error"` +} + +func submitForm(identifier string, formData map[string]interface{}) (*FormResponse, error) { + payload := FormPayload{FormData: formData} + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("https://sim.ai/api/form/%s", identifier) + resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result FormResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func main() { + result, err := submitForm("your-identifier", map[string]interface{}{ + "name": "John Doe", + "email": "john@example.com", + "message": "Hello from Go!", + }) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + if result.Success { + fmt.Printf("Submitted! Execution ID: %s\n", result.Data.ExecutionID) + } else { + fmt.Printf("Error: %s\n", result.Error) + } +} +``` + + + +### Password-Protected Forms + +For password-protected forms, include the password in your request: + +```bash +curl -X POST https://sim.ai/api/form/your-identifier \ + -H "Content-Type: application/json" \ + -d '{ + "password": "your-password", + "formData": { + "name": "John Doe" + } + }' +``` + +### Email-Protected Forms + +For email-protected forms, include the email: + +```bash +curl -X POST https://sim.ai/api/form/your-identifier \ + -H "Content-Type: application/json" \ + -d '{ + "email": "allowed@example.com", + "formData": { + "name": "John Doe" + } + }' +``` + +## Execution Logs + +Form submissions appear in your workflow's execution logs with: +- **Trigger Type**: `form` +- **Input Data**: The submitted form values +- **Execution ID**: Unique identifier for tracking + +Filter your logs by trigger type `form` to see all form submissions. + +## React Component Example + +Embed forms in React applications: + +```tsx +import { useEffect, useRef } from 'react'; + +interface SimFormProps { + identifier: string; + height?: number; + className?: string; +} + +export function SimForm({ identifier, height = 600, className }: SimFormProps) { + const iframeRef = useRef(null); + + return ( + ` + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(iframeCode) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [iframeCode]) + + return ( +
+
+ + + + + + + {copied ? 'Copied' : 'Copy'} + + +
+ + +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx new file mode 100644 index 0000000000..65c0388035 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx @@ -0,0 +1,685 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { createLogger } from '@sim/logger' +import { ChevronDown, ChevronRight, Eye, EyeOff, Loader2, X } from 'lucide-react' +import { Badge, Button, Input, Label, Textarea } from '@/components/emcn' +import { Skeleton } from '@/components/ui' +import { getEnv } from '@/lib/core/config/env' +import { isDev } from '@/lib/core/config/feature-flags' +import { cn } from '@/lib/core/utils/cn' +import { getBaseUrl, getEmailDomain } from '@/lib/core/utils/urls' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import { EmbedCodeGenerator } from './components/embed-code-generator' +import { useFormDeployment } from './hooks/use-form-deployment' +import { useIdentifierValidation } from './hooks/use-identifier-validation' + +const logger = createLogger('FormDeploy') + +interface FormErrors { + identifier?: string + title?: string + password?: string + emails?: string + general?: string +} + +interface FieldConfig { + name: string + type: string + label: string + description?: string + required?: boolean +} + +export interface ExistingForm { + id: string + identifier: string + title: string + description?: string + customizations: { + primaryColor?: string + thankYouMessage?: string + logoUrl?: string + fieldConfigs?: FieldConfig[] + } + authType: 'public' | 'password' | 'email' + hasPassword?: boolean + allowedEmails?: string[] + showBranding: boolean + isActive: boolean +} + +interface FormDeployProps { + workflowId: string + onDeploymentComplete?: () => void + onValidationChange?: (isValid: boolean) => void + onSubmittingChange?: (isSubmitting: boolean) => void + onExistingFormChange?: (exists: boolean) => void + formSubmitting?: boolean + setFormSubmitting?: (submitting: boolean) => void + onDeployed?: () => Promise +} + +const getDomainPrefix = (() => { + const prefix = `${getEmailDomain()}/form/` + return () => prefix +})() + +export function FormDeploy({ + workflowId, + onDeploymentComplete, + onValidationChange, + onSubmittingChange, + onExistingFormChange, + formSubmitting, + setFormSubmitting, + onDeployed, +}: FormDeployProps) { + const [identifier, setIdentifier] = useState('') + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [thankYouMessage, setThankYouMessage] = useState( + 'Your response has been submitted successfully.' + ) + const [authType, setAuthType] = useState<'public' | 'password' | 'email'>('public') + const [password, setPassword] = useState('') + const [allowedEmails, setAllowedEmails] = useState([]) + const [existingForm, setExistingForm] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [formUrl, setFormUrl] = useState('') + const [inputFields, setInputFields] = useState<{ name: string; type: string }[]>([]) + const [showPasswordField, setShowPasswordField] = useState(false) + const [fieldConfigs, setFieldConfigs] = useState([]) + const [expandedFields, setExpandedFields] = useState>(new Set()) + const [errors, setErrors] = useState({}) + const [isIdentifierValid, setIsIdentifierValid] = useState(false) + + const { createForm, updateForm, deleteForm, isSubmitting } = useFormDeployment() + + const { + isChecking: isCheckingIdentifier, + error: identifierError, + isValid: identifierValidationPassed, + } = useIdentifierValidation(identifier, existingForm?.identifier, !!existingForm) + + useEffect(() => { + setIsIdentifierValid(identifierValidationPassed) + }, [identifierValidationPassed]) + + const setError = (field: keyof FormErrors, message: string) => { + setErrors((prev) => ({ ...prev, [field]: message })) + } + + const clearError = (field: keyof FormErrors) => { + setErrors((prev) => ({ ...prev, [field]: undefined })) + } + + // Fetch existing form deployment + useEffect(() => { + async function fetchExistingForm() { + if (!workflowId) return + + try { + setIsLoading(true) + const response = await fetch(`/api/workflows/${workflowId}/form/status`) + + if (response.ok) { + const data = await response.json() + if (data.isDeployed && data.form) { + const detailResponse = await fetch(`/api/form/manage/${data.form.id}`) + if (detailResponse.ok) { + const formDetail = await detailResponse.json() + const form = formDetail.form as ExistingForm + setExistingForm(form) + onExistingFormChange?.(true) + + setIdentifier(form.identifier) + setTitle(form.title) + setDescription(form.description || '') + setThankYouMessage( + form.customizations?.thankYouMessage || + 'Your response has been submitted successfully.' + ) + setAuthType(form.authType) + setAllowedEmails(form.allowedEmails || []) + if (form.customizations?.fieldConfigs) { + setFieldConfigs(form.customizations.fieldConfigs) + } + + const baseUrl = getBaseUrl() + try { + const url = new URL(baseUrl) + let host = url.host + if (host.startsWith('www.')) host = host.substring(4) + setFormUrl(`${url.protocol}//${host}/form/${form.identifier}`) + } catch { + setFormUrl( + isDev + ? `http://localhost:3000/form/${form.identifier}` + : `https://sim.ai/form/${form.identifier}` + ) + } + } + } else { + setExistingForm(null) + onExistingFormChange?.(false) + + const workflowName = + useWorkflowStore.getState().blocks[Object.keys(useWorkflowStore.getState().blocks)[0]] + ?.name || 'Form' + setTitle(`${workflowName} Form`) + } + } + } catch (err) { + logger.error('Error fetching form deployment:', err) + } finally { + setIsLoading(false) + } + } + + fetchExistingForm() + }, [workflowId, onExistingFormChange]) + + // Get input fields from start block and initialize field configs + useEffect(() => { + const blocks = Object.values(useWorkflowStore.getState().blocks) + const startBlock = blocks.find((b) => b.type === 'starter' || b.type === 'start_trigger') + + if (startBlock) { + const inputFormat = useSubBlockStore.getState().getValue(startBlock.id, 'inputFormat') + if (inputFormat && Array.isArray(inputFormat)) { + setInputFields(inputFormat) + + // Initialize field configs if not already set + if (fieldConfigs.length === 0) { + setFieldConfigs( + inputFormat.map((f: { name: string; type?: string }) => ({ + name: f.name, + type: f.type || 'string', + label: f.name + .replace(/([A-Z])/g, ' $1') + .replace(/_/g, ' ') + .replace(/^./, (s) => s.toUpperCase()) + .trim(), + })) + ) + } + } + } + }, [workflowId, fieldConfigs.length]) + + // Validate form + useEffect(() => { + const isValid = + inputFields.length > 0 && + isIdentifierValid && + title.trim().length > 0 && + (authType !== 'password' || password.length > 0 || !!existingForm?.hasPassword) && + (authType !== 'email' || allowedEmails.length > 0) + + onValidationChange?.(isValid) + }, [ + isIdentifierValid, + title, + authType, + password, + allowedEmails, + existingForm?.hasPassword, + onValidationChange, + inputFields.length, + ]) + + useEffect(() => { + onSubmittingChange?.(isSubmitting) + setFormSubmitting?.(isSubmitting) + }, [isSubmitting, onSubmittingChange, setFormSubmitting]) + + const toggleFieldExpanded = (fieldName: string) => { + setExpandedFields((prev) => { + const next = new Set(prev) + if (next.has(fieldName)) { + next.delete(fieldName) + } else { + next.add(fieldName) + } + return next + }) + } + + const updateFieldConfig = (fieldName: string, updates: Partial) => { + setFieldConfigs((prev) => prev.map((f) => (f.name === fieldName ? { ...f, ...updates } : f))) + } + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault() + setErrors({}) + + // Validate before submit + if (!isIdentifierValid && identifier !== existingForm?.identifier) { + setError('identifier', 'Please wait for identifier validation to complete') + return + } + + if (!title.trim()) { + setError('title', 'Title is required') + return + } + + if (authType === 'password' && !existingForm?.hasPassword && !password.trim()) { + setError('password', 'Password is required') + return + } + + if (authType === 'email' && allowedEmails.length === 0) { + setError('emails', 'At least one email or domain is required') + return + } + + const customizations = { + thankYouMessage, + fieldConfigs, + } + + try { + if (existingForm) { + await updateForm(existingForm.id, { + identifier, + title, + description, + customizations, + authType, + password: password || undefined, + allowedEmails, + }) + } else { + const result = await createForm({ + workflowId, + identifier, + title, + description, + customizations, + authType, + password, + allowedEmails, + }) + + if (result?.formUrl) { + setFormUrl(result.formUrl) + // Open the form in a new window after successful deployment + window.open(result.formUrl, '_blank', 'noopener,noreferrer') + } + } + + await onDeployed?.() + + if (!existingForm) { + onDeploymentComplete?.() + } + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'An error occurred' + logger.error('Error deploying form:', err) + + // Parse error message and show inline + if (message.toLowerCase().includes('identifier')) { + setError('identifier', message) + } else if (message.toLowerCase().includes('password')) { + setError('password', message) + } else if (message.toLowerCase().includes('email')) { + setError('emails', message) + } else { + setError('general', message) + } + } + }, + [ + existingForm, + workflowId, + identifier, + title, + description, + thankYouMessage, + fieldConfigs, + authType, + password, + allowedEmails, + isIdentifierValid, + createForm, + updateForm, + onDeployed, + onDeploymentComplete, + ] + ) + + const handleDelete = useCallback(async () => { + if (!existingForm) return + + try { + await deleteForm(existingForm.id) + setExistingForm(null) + onExistingFormChange?.(false) + setIdentifier('') + setTitle('') + setDescription('') + setFormUrl('') + } catch (err) { + logger.error('Error deleting form:', err) + } + }, [existingForm, deleteForm, onExistingFormChange]) + + if (isLoading) { + return ( +
+
+
+ + + +
+
+ + +
+
+
+ ) + } + + if (inputFields.length === 0) { + return ( +
+ Add input fields to the Start block to create a form. +
+ ) + } + + const fullUrl = `${getEnv('NEXT_PUBLIC_APP_URL')}/form/${identifier}` + const displayUrl = fullUrl.replace(/^https?:\/\//, '') + + return ( +
+
+ {/* URL Input - matching chat style */} +
+ +
+
+ {getDomainPrefix()} +
+
+ { + setIdentifier(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '')) + clearError('identifier') + }} + placeholder='my-form' + className={cn( + 'rounded-none border-0 pl-0 shadow-none', + isCheckingIdentifier && 'pr-[32px]' + )} + /> + {isCheckingIdentifier && ( +
+ +
+ )} +
+
+ {(identifierError || errors.identifier) && ( +

+ {identifierError || errors.identifier} +

+ )} +

+ {existingForm && identifier ? ( + <> + Live at:{' '} + + {displayUrl} + + + ) : ( + 'The unique URL path where your form will be accessible' + )} +

+
+ + {/* Title */} +
+ + { + setTitle(e.target.value) + clearError('title') + }} + placeholder='Contact Form' + /> + {errors.title && ( +

{errors.title}

+ )} +
+ + {/* Form Fields Configuration */} +
+ +
+ {fieldConfigs.map((config) => ( +
+
toggleFieldExpanded(config.name)} + > +
+ {expandedFields.has(config.name) ? ( + + ) : ( + + )} + + {config.label || config.name} + +
+ {config.type} +
+ + {expandedFields.has(config.name) && ( +
+
+ + updateFieldConfig(config.name, { label: e.target.value })} + placeholder='Enter display label' + /> +
+
+ + + updateFieldConfig(config.name, { description: e.target.value }) + } + placeholder='Optional help text' + /> +
+

+ Maps to:{' '} + {config.name} +

+
+ )} +
+ ))} +
+
+ + {/* Access Control */} +
+ +
+ {(['public', 'password', 'email'] as const).map((type, i, arr) => ( + + ))} +
+
+ + {authType === 'password' && ( +
+ +
+ { + setPassword(e.target.value) + clearError('password') + }} + placeholder={ + existingForm?.hasPassword ? 'Enter new password to change' : 'Enter password' + } + className='pr-[32px]' + /> + +
+ {errors.password && ( +

{errors.password}

+ )} +

+ {existingForm?.hasPassword + ? 'Leave empty to keep the current password' + : 'This password will be required to access your form'} +

+
+ )} + + {authType === 'email' && ( +
+ +
+ {allowedEmails.map((email) => ( +
+ {email} + +
+ ))} + 0 ? 'Add another' : 'Enter emails or @domain.com' + } + className='min-w-[150px] flex-1 border-none bg-transparent p-0 text-sm outline-none placeholder:text-[var(--text-muted)]' + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault() + const value = (e.target as HTMLInputElement).value.trim() + if (value && !allowedEmails.includes(value)) { + setAllowedEmails([...allowedEmails, value]) + clearError('emails') + ;(e.target as HTMLInputElement).value = '' + } + } + }} + /> +
+ {errors.emails && ( +

{errors.emails}

+ )} +

+ Add specific emails or entire domains (@example.com) +

+
+ )} + + {/* Thank You Message */} +
+ +