improvement[webhook]: refactored webhook modal into components

This commit is contained in:
Waleed Latif
2025-03-20 18:01:51 -07:00
parent 20caa0e31f
commit 01e1054f58
13 changed files with 812 additions and 695 deletions

60
package-lock.json generated
View File

@@ -1,60 +0,0 @@
{
"name": "sim",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"framer-motion": "^12.5.0"
}
},
"node_modules/framer-motion": {
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.5.0.tgz",
"integrity": "sha512-buPlioFbH9/W7rDzYh1C09AuZHAk2D1xTA1BlounJ2Rb9aRg84OXexP0GLd+R83v0khURdMX7b5MKnGTaSg5iA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.5.0",
"motion-utils": "^12.5.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.5.0.tgz",
"integrity": "sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.5.0"
}
},
"node_modules/motion-utils": {
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.5.0.tgz",
"integrity": "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"framer-motion": "^12.5.0"
}
}

View File

@@ -0,0 +1,134 @@
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { CopyableField } from '../ui/copyable'
import { TestResultDisplay } from '../ui/test-result'
interface GenericConfigProps {
requireAuth: boolean
setRequireAuth: (requireAuth: boolean) => void
generalToken: string
setGeneralToken: (token: string) => void
secretHeaderName: string
setSecretHeaderName: (headerName: string) => void
allowedIps: string
setAllowedIps: (ips: string) => void
isLoadingToken: boolean
testResult: {
success: boolean
message?: string
test?: any
} | null
copied: string | null
copyToClipboard: (text: string, type: string) => void
testWebhook: () => Promise<void>
}
export function GenericConfig({
requireAuth,
setRequireAuth,
generalToken,
setGeneralToken,
secretHeaderName,
setSecretHeaderName,
allowedIps,
setAllowedIps,
isLoadingToken,
testResult,
copied,
copyToClipboard,
}: GenericConfigProps) {
return (
<div className="space-y-4">
<div className="flex items-center space-x-2 mb-3">
<div className="flex items-center h-3.5 space-x-2">
<Checkbox
id="require-auth"
checked={requireAuth}
onCheckedChange={(checked) => setRequireAuth(checked as boolean)}
/>
<Label htmlFor="require-auth" className="text-sm font-medium cursor-pointer">
Require Authentication
</Label>
</div>
</div>
{requireAuth && (
<div className="space-y-4 ml-5 border-l-2 pl-4 border-gray-200 dark:border-gray-700">
<CopyableField
id="auth-token"
label="Authentication Token"
value={generalToken}
onChange={setGeneralToken}
placeholder="Enter an auth token"
description="This token will be used to authenticate requests to your webhook (via Bearer token)."
isLoading={isLoadingToken}
copied={copied}
copyType="general-token"
copyToClipboard={copyToClipboard}
/>
<div className="space-y-2">
<Label htmlFor="header-name">Secret Header Name (Optional)</Label>
<Input
id="header-name"
value={secretHeaderName}
onChange={(e) => setSecretHeaderName(e.target.value)}
placeholder="X-Secret-Key"
className="flex-1"
/>
<p className="text-xs text-muted-foreground">
Custom HTTP header name for passing the authentication token instead of using Bearer
authentication.
</p>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="allowed-ips">Allowed IP Addresses (Optional)</Label>
<Input
id="allowed-ips"
value={allowedIps}
onChange={(e) => setAllowedIps(e.target.value)}
placeholder="192.168.1.1, 10.0.0.1"
className="flex-1"
/>
<p className="text-xs text-muted-foreground">
Comma-separated list of IP addresses that are allowed to access this webhook.
</p>
</div>
<TestResultDisplay
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
showCurlCommand={true}
/>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<h4 className="font-medium text-sm mb-2">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Copy the Webhook URL above</li>
<li>Configure your service to send HTTP POST requests to this URL</li>
{requireAuth && (
<>
<li>
{secretHeaderName
? `Add the "${secretHeaderName}" header with your token to all requests`
: 'Add an "Authorization: Bearer YOUR_TOKEN" header to all requests'}
</li>
</>
)}
</ol>
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
The webhook will receive all HTTP POST requests and pass the data to your workflow.
</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,64 @@
import { Loader2 } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface GithubConfigProps {
contentType: string
setContentType: (contentType: string) => void
isLoadingToken: boolean
testResult: {
success: boolean
message?: string
test?: any
} | null
copied: string | null
copyToClipboard: (text: string, type: string) => void
}
export function GithubConfig({
contentType,
setContentType,
isLoadingToken,
testResult,
}: GithubConfigProps) {
return (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="github-content-type">Content Type</Label>
{isLoadingToken ? (
<div className="h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id="github-content-type"
value={contentType}
onChange={(e) => setContentType(e.target.value)}
placeholder="application/json"
className="flex-1"
/>
)}
</div>
<div className="space-y-2">
<h4 className="font-medium">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to your GitHub repository</li>
<li>Navigate to Settings {'>'} Webhooks</li>
<li>Click "Add webhook"</li>
<li>Enter the Webhook URL shown above</li>
<li>Set Content type to "{contentType}"</li>
<li>Choose which events you want to trigger the webhook</li>
<li>Ensure "Active" is checked and save</li>
</ol>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
After saving, GitHub will send a ping event to verify your webhook.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,38 @@
interface StripeConfigProps {
isLoadingToken: boolean
testResult: {
success: boolean
message?: string
test?: any
} | null
copied: string | null
copyToClipboard: (text: string, type: string) => void
}
export function StripeConfig({
isLoadingToken,
testResult,
copied,
copyToClipboard,
}: StripeConfigProps) {
return (
<div className="space-y-2">
<h4 className="font-medium">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to your Stripe Dashboard</li>
<li>Navigate to Developers {'>'} Webhooks</li>
<li>Click "Add endpoint"</li>
<li>Enter the Webhook URL shown above</li>
<li>Select the events you want to listen for</li>
<li>Add the endpoint</li>
</ol>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
Stripe will send a test event to verify your webhook endpoint.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,104 @@
import { CopyableField } from '../ui/copyable'
import { TestResultDisplay } from '../ui/test-result'
interface WhatsAppConfigProps {
verificationToken: string
setVerificationToken: (token: string) => void
isLoadingToken: boolean
testResult: {
success: boolean
message?: string
test?: any
} | null
copied: string | null
copyToClipboard: (text: string, type: string) => void
}
export function WhatsAppConfig({
verificationToken,
setVerificationToken,
isLoadingToken,
testResult,
copied,
copyToClipboard,
}: WhatsAppConfigProps) {
return (
<div className="space-y-4">
<CopyableField
id="whatsapp-verification-token"
label="Verification Token"
value={verificationToken}
onChange={setVerificationToken}
placeholder="Enter a verification token for WhatsApp"
description="This token will be used to verify your webhook with WhatsApp."
isLoading={isLoadingToken}
copied={copied}
copyType="token"
copyToClipboard={copyToClipboard}
/>
<TestResultDisplay
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
/>
<div className="space-y-2">
<h4 className="font-medium">Setup Instructions</h4>
<ol className="space-y-2">
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">1.</span>
<span className="text-sm">Go to WhatsApp Business Platform dashboard</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">2.</span>
<span className="text-sm">Navigate to "Configuration" in the sidebar</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">3.</span>
<span className="text-sm">
Enter the URL above as "Callback URL" (exactly as shown)
</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">4.</span>
<span className="text-sm">Enter your token as "Verify token"</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">5.</span>
<span className="text-sm">Click "Verify and save" and subscribe to "messages"</span>
</li>
</ol>
<div className="bg-blue-50 dark:bg-blue-950 p-3 rounded-md mt-3 border border-blue-200 dark:border-blue-800">
<h5 className="text-sm font-medium text-blue-800 dark:text-blue-300">Requirements</h5>
<ul className="mt-1 space-y-1">
<li className="flex items-start">
<span className="text-blue-500 dark:text-blue-400 mr-2"></span>
<span className="text-sm text-blue-700 dark:text-blue-300">
URL must be publicly accessible with HTTPS
</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 dark:text-blue-400 mr-2"></span>
<span className="text-sm text-blue-700 dark:text-blue-300">
Self-signed SSL certificates not supported
</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 dark:text-blue-400 mr-2"></span>
<span className="text-sm text-blue-700 dark:text-blue-300">
For local testing, use ngrok to expose your server
</span>
</li>
</ul>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
After saving, use "Test" to verify your webhook configuration.
</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,76 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
interface DeleteConfirmDialogProps {
open: boolean
setOpen: (open: boolean) => void
onConfirm: () => void
isDeleting: boolean
}
export function DeleteConfirmDialog({
open,
setOpen,
onConfirm,
isDeleting,
}: DeleteConfirmDialogProps) {
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This will delete the webhook configuration. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700">
{isDeleting ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
interface UnsavedChangesDialogProps {
open: boolean
setOpen: (open: boolean) => void
onCancel: () => void
onConfirm: () => void
}
export function UnsavedChangesDialog({
open,
setOpen,
onCancel,
onConfirm,
}: UnsavedChangesDialogProps) {
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
<AlertDialogDescription>
You have unsaved changes. Are you sure you want to close without saving?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700">
Discard changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -0,0 +1,64 @@
import { Check, Copy, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface CopyableFieldProps {
id: string
label: string
value: string
onChange?: (value: string) => void
placeholder?: string
description?: string
isLoading?: boolean
copied: string | null
copyType: string
copyToClipboard: (text: string, type: string) => void
readOnly?: boolean
}
export function CopyableField({
id,
label,
value,
onChange,
placeholder,
description,
isLoading = false,
copied,
copyType,
copyToClipboard,
readOnly = false,
}: CopyableFieldProps) {
return (
<div className="space-y-2">
<Label htmlFor={id}>{label}</Label>
<div className="flex items-center space-x-2">
{isLoading ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id={id}
value={value}
onChange={onChange ? (e) => onChange(e.target.value) : undefined}
placeholder={placeholder}
className="flex-1"
readOnly={readOnly}
/>
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(value, copyType)}
disabled={isLoading || !value}
>
{copied === copyType ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
{description && <p className="text-xs text-muted-foreground">{description}</p>}
</div>
)
}

View File

@@ -0,0 +1,69 @@
import { motion } from 'framer-motion'
import { Check, Copy } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface TestResultDisplayProps {
testResult: {
success: boolean
message?: string
test?: {
curlCommand?: string
status?: number
contentType?: string
responseText?: string
headers?: Record<string, string>
samplePayload?: Record<string, any>
}
} | null
copied: string | null
copyToClipboard: (text: string, type: string) => void
showCurlCommand?: boolean
}
export function TestResultDisplay({
testResult,
copied,
copyToClipboard,
showCurlCommand = false,
}: TestResultDisplayProps) {
if (!testResult) return null
return (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={`p-3 rounded-md ${
testResult.success
? 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300 border border-green-200 dark:border-green-800'
: 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300 border border-red-200 dark:border-red-800'
}`}
>
<p className="text-sm">{testResult.message}</p>
{showCurlCommand && testResult.success && testResult.test?.curlCommand && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="mt-3 bg-black/10 dark:bg-white/10 p-2 rounded text-xs font-mono overflow-x-auto relative group"
>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-2 h-6 w-6 opacity-70 hover:opacity-100"
onClick={() => copyToClipboard(testResult.test?.curlCommand || '', 'curl-command')}
>
{copied === 'curl-command' ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
<pre className="whitespace-pre-wrap break-all pr-8">{testResult.test.curlCommand}</pre>
</motion.div>
)}
</motion.div>
)
}

View File

@@ -0,0 +1,92 @@
import { Loader2, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { DialogFooter } from '@/components/ui/dialog'
interface WebhookDialogFooterProps {
webhookId: string | undefined
webhookProvider: string
isSaving: boolean
isDeleting: boolean
isLoadingToken: boolean
isTesting: boolean
onSave: () => void
onDelete: () => void
onTest?: () => void
onClose: () => void
}
export function WebhookDialogFooter({
webhookId,
webhookProvider,
isSaving,
isDeleting,
isLoadingToken,
isTesting,
onSave,
onDelete,
onTest,
onClose,
}: WebhookDialogFooterProps) {
const showTestButton =
webhookId && (webhookProvider === 'whatsapp' || webhookProvider === 'generic') && onTest
return (
<DialogFooter className="flex justify-between mt-6 sticky bottom-0 py-3 bg-background border-t z-10">
<div>
{webhookId && (
<div className="flex space-x-3">
<Button
variant="destructive"
onClick={onDelete}
disabled={isDeleting || isLoadingToken}
>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</>
)}
</Button>
{showTestButton && (
<Button variant="outline" onClick={onTest} disabled={isTesting || isLoadingToken}>
{isTesting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Testing...
</>
) : (
'Test'
)}
</Button>
)}
</div>
)}
</div>
<div className="flex space-x-3">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
variant="default"
onClick={onSave}
disabled={isSaving || isLoadingToken}
className="bg-primary"
>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
'Save'
)}
</Button>
</div>
</DialogFooter>
)
}

View File

@@ -0,0 +1,50 @@
import { Badge } from '@/components/ui/badge'
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { WEBHOOK_PROVIDERS } from '../../webhook-config'
interface WebhookDialogHeaderProps {
webhookProvider: string
webhookId: string | undefined
}
export function WebhookDialogHeader({ webhookProvider, webhookId }: WebhookDialogHeaderProps) {
const provider = WEBHOOK_PROVIDERS[webhookProvider] || WEBHOOK_PROVIDERS.generic
// Get provider icon
const getProviderIcon = () => {
return provider.icon({
className: 'h-5 w-5 text-green-500 dark:text-green-400',
})
}
// Get provider-specific title
const getProviderTitle = () => {
return `${provider.name} Integration`
}
return (
<DialogHeader className="flex flex-row items-center justify-between">
<div className="flex items-center">
<div className="mr-3 flex items-center">{getProviderIcon()}</div>
<div>
<DialogTitle>{getProviderTitle()}</DialogTitle>
<DialogDescription>
{webhookProvider === 'generic'
? 'Configure your webhook integration'
: `Configure your ${provider.name.toLowerCase()} integration`}
</DialogDescription>
</div>
</div>
<div className="flex items-center space-x-2">
{webhookId && (
<Badge
variant="outline"
className="bg-green-50 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800"
>
Connected
</Badge>
)}
</div>
</DialogHeader>
)
}

View File

@@ -0,0 +1,42 @@
import { Check, Copy, Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface WebhookUrlFieldProps {
webhookUrl: string
isLoadingToken: boolean
copied: string | null
copyToClipboard: (text: string, type: string) => void
}
export function WebhookUrlField({
webhookUrl,
isLoadingToken,
copied,
copyToClipboard,
}: WebhookUrlFieldProps) {
return (
<div className="space-y-2">
<Label htmlFor="webhook-url">Webhook URL</Label>
<div className="flex items-center space-x-2">
{isLoadingToken ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input id="webhook-url" value={webhookUrl} readOnly className="flex-1" />
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(webhookUrl, 'url')}
disabled={isLoadingToken}
>
{copied === 'url' ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
)
}

View File

@@ -1,31 +1,16 @@
import { useEffect, useState } from 'react'
import { motion } from 'framer-motion'
import { Check, Copy, Loader2, Trash2 } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { createLogger } from '@/lib/logs/console-logger'
import { ProviderConfig, WEBHOOK_PROVIDERS } from '../webhook-config'
import { GenericConfig } from './providers/generic-config'
import { GithubConfig } from './providers/github-config'
import { StripeConfig } from './providers/stripe-config'
import { WhatsAppConfig } from './providers/whatsapp-config'
import { DeleteConfirmDialog } from './ui/confirmation'
import { UnsavedChangesDialog } from './ui/confirmation'
import { WebhookDialogFooter } from './ui/webhook-footer'
import { WebhookDialogHeader } from './ui/webhook-header'
import { WebhookUrlField } from './ui/webhook-url'
const logger = createLogger('WebhookModal')
@@ -370,488 +355,58 @@ export function WebhookModal({
}
}
// Get provider icon
const getProviderIcon = () => {
return provider.icon({
className: 'h-5 w-5 text-green-500 dark:text-green-400',
})
}
// Get provider-specific title
const getProviderTitle = () => {
return `${provider.name} Integration`
}
// Provider-specific setup instructions and configuration fields
// Provider-specific component rendering
const renderProviderContent = () => {
switch (webhookProvider) {
case 'whatsapp':
return (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="whatsapp-verification-token">Verification Token</Label>
<div className="flex items-center space-x-2">
{isLoadingToken ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id="whatsapp-verification-token"
value={whatsappVerificationToken}
onChange={(e) => setWhatsappVerificationToken(e.target.value)}
placeholder="Enter a verification token for WhatsApp"
className="flex-1"
/>
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(whatsappVerificationToken, 'token')}
disabled={isLoadingToken}
>
{copied === 'token' ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
This token will be used to verify your webhook with WhatsApp.
</p>
</div>
{testResult && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={`p-3 rounded-md ${
testResult.success
? 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300 border border-green-200 dark:border-green-800'
: 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300 border border-red-200 dark:border-red-800'
}`}
>
<p className="text-sm">{testResult.message}</p>
</motion.div>
)}
<div className="space-y-2">
<h4 className="font-medium">Setup Instructions</h4>
<ol className="space-y-2">
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">1.</span>
<span className="text-sm">Go to WhatsApp Business Platform dashboard</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">2.</span>
<span className="text-sm">Navigate to "Configuration" in the sidebar</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">3.</span>
<span className="text-sm">
Enter the URL above as "Callback URL" (exactly as shown)
</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">4.</span>
<span className="text-sm">Enter your token as "Verify token"</span>
</li>
<li className="flex items-start">
<span className="text-gray-500 dark:text-gray-400 mr-2">5.</span>
<span className="text-sm">
Click "Verify and save" and subscribe to "messages"
</span>
</li>
</ol>
<div className="bg-blue-50 dark:bg-blue-950 p-3 rounded-md mt-3 border border-blue-200 dark:border-blue-800">
<h5 className="text-sm font-medium text-blue-800 dark:text-blue-300">
Requirements
</h5>
<ul className="mt-1 space-y-1">
<li className="flex items-start">
<span className="text-blue-500 dark:text-blue-400 mr-2"></span>
<span className="text-sm text-blue-700 dark:text-blue-300">
URL must be publicly accessible with HTTPS
</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 dark:text-blue-400 mr-2"></span>
<span className="text-sm text-blue-700 dark:text-blue-300">
Self-signed SSL certificates not supported
</span>
</li>
<li className="flex items-start">
<span className="text-blue-500 dark:text-blue-400 mr-2"></span>
<span className="text-sm text-blue-700 dark:text-blue-300">
For local testing, use ngrok to expose your server
</span>
</li>
</ul>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
After saving, use "Test" to verify your webhook configuration.
</p>
</div>
</div>
</div>
<WhatsAppConfig
verificationToken={whatsappVerificationToken}
setVerificationToken={setWhatsappVerificationToken}
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
/>
)
case 'github':
return (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="github-content-type">Content Type</Label>
{isLoadingToken ? (
<div className="h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id="github-content-type"
value={githubContentType}
onChange={(e) => setGithubContentType(e.target.value)}
placeholder="application/json"
className="flex-1"
/>
)}
</div>
<div className="space-y-2">
<h4 className="font-medium">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to your GitHub repository</li>
<li>Navigate to Settings {'>'} Webhooks</li>
<li>Click "Add webhook"</li>
<li>Enter the Webhook URL shown above</li>
<li>Set Content type to "{githubContentType}"</li>
<li>Choose which events you want to trigger the webhook</li>
<li>Ensure "Active" is checked and save</li>
</ol>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
After saving, GitHub will send a ping event to verify your webhook.
</p>
</div>
</div>
<GithubConfig
contentType={githubContentType}
setContentType={setGithubContentType}
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
/>
)
case 'stripe':
return (
<div className="space-y-2">
<h4 className="font-medium">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Go to your Stripe Dashboard</li>
<li>Navigate to Developers {'>'} Webhooks</li>
<li>Click "Add endpoint"</li>
<li>Enter the Webhook URL shown above</li>
<li>Select the events you want to listen for</li>
<li>Add the endpoint</li>
</ol>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
Stripe will send a test event to verify your webhook endpoint.
</p>
</div>
</div>
<StripeConfig
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
/>
)
case 'generic':
return (
<div className="space-y-4">
<div className="flex items-center space-x-2 mb-3">
<div className="flex items-center h-3.5 space-x-2">
<Checkbox
id="require-auth"
checked={requireAuth}
onCheckedChange={(checked) => setRequireAuth(checked as boolean)}
/>
<Label htmlFor="require-auth" className="text-sm font-medium cursor-pointer">
Require Authentication
</Label>
</div>
</div>
{requireAuth && (
<div className="space-y-4 ml-5 border-l-2 pl-4 border-gray-200 dark:border-gray-700">
<div className="space-y-2">
<Label htmlFor="auth-token">Authentication Token</Label>
<div className="flex items-center space-x-2">
{isLoadingToken ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id="auth-token"
value={generalToken}
onChange={(e) => setGeneralToken(e.target.value)}
placeholder="Enter an auth token"
className="flex-1"
/>
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(generalToken, 'general-token')}
disabled={isLoadingToken}
>
{copied === 'general-token' ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
This token will be used to authenticate requests to your webhook (via Bearer
token).
</p>
</div>
<div className="space-y-2">
<Label htmlFor="header-name">Secret Header Name (Optional)</Label>
<Input
id="header-name"
value={secretHeaderName}
onChange={(e) => setSecretHeaderName(e.target.value)}
placeholder="X-Secret-Key"
className="flex-1"
/>
<p className="text-xs text-muted-foreground">
Custom HTTP header name for passing the authentication token instead of using
Bearer authentication.
</p>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="allowed-ips">Allowed IP Addresses (Optional)</Label>
<Input
id="allowed-ips"
value={allowedIps}
onChange={(e) => setAllowedIps(e.target.value)}
placeholder="192.168.1.1, 10.0.0.1"
className="flex-1"
/>
<p className="text-xs text-muted-foreground">
Comma-separated list of IP addresses that are allowed to access this webhook.
</p>
</div>
{testResult && testResult.success && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="p-3 rounded-md bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300 border border-green-200 dark:border-green-800"
>
<p className="text-sm">{testResult.message}</p>
</motion.div>
)}
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<h4 className="font-medium text-sm mb-2">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Copy the Webhook URL above</li>
<li>Configure your service to send HTTP POST requests to this URL</li>
{requireAuth && (
<>
<li>
{secretHeaderName
? `Add the "${secretHeaderName}" header with your token to all requests`
: 'Add an "Authorization: Bearer YOUR_TOKEN" header to all requests'}
</li>
</>
)}
</ol>
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
The webhook will receive all HTTP POST requests and pass the data to your
workflow.
</p>
</div>
</div>
{testResult && testResult.success && testResult.test?.curlCommand && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="mt-3 bg-black/10 dark:bg-white/10 p-2 rounded text-xs font-mono overflow-x-auto relative group"
>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-2 top-2 h-6 w-6 opacity-70 hover:opacity-100"
onClick={() =>
copyToClipboard(testResult.test?.curlCommand || '', 'curl-command')
}
>
{copied === 'curl-command' ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
<pre className="whitespace-pre-wrap break-all pr-8">
{testResult.test.curlCommand}
</pre>
</motion.div>
)}
{testResult && !testResult.success && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="p-3 rounded-md bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300 border border-red-200 dark:border-red-800"
>
<p className="text-sm">{testResult.message}</p>
</motion.div>
)}
</div>
)
default:
return (
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="require-auth"
checked={requireAuth}
onCheckedChange={(checked) => setRequireAuth(checked === true)}
className="h-3.5 w-3.5"
/>
<div>
<Label htmlFor="require-auth" className="text-sm font-medium cursor-pointer">
Require Authentication
</Label>
<p className="text-xs text-muted-foreground mt-0.5">
Enable authentication to secure your webhook endpoint.
</p>
</div>
</div>
{requireAuth && (
<div className="space-y-4 pl-5 ml-1 border-l border-gray-200 dark:border-gray-700">
<div className="space-y-2">
<Label htmlFor="auth-token">Authentication Token</Label>
<div className="flex items-center space-x-2">
{isLoadingToken ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input
id="auth-token"
value={generalToken}
onChange={(e) => setGeneralToken(e.target.value)}
placeholder="Enter an authentication token"
className="flex-1"
/>
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(generalToken, 'general-token')}
disabled={isLoadingToken}
>
{copied === 'general-token' ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
This token will be used to authenticate requests to your webhook (via Bearer
token).
</p>
</div>
<div className="space-y-2">
<Label htmlFor="header-name">Secret Header Name (Optional)</Label>
<Input
id="header-name"
value={secretHeaderName}
onChange={(e) => setSecretHeaderName(e.target.value)}
placeholder="X-Secret-Key"
className="flex-1"
/>
<p className="text-xs text-muted-foreground">
Custom HTTP header name for passing the authentication token instead of using
Bearer authentication.
</p>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="allowed-ips">Allowed IP Addresses (Optional)</Label>
<Input
id="allowed-ips"
value={allowedIps}
onChange={(e) => setAllowedIps(e.target.value)}
placeholder="192.168.1.1, 10.0.0.1"
className="flex-1"
/>
<p className="text-xs text-muted-foreground">
Comma-separated list of IP addresses that are allowed to access this webhook.
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md mt-3 border border-gray-200 dark:border-gray-700">
<h4 className="font-medium text-sm mb-2">Setup Instructions</h4>
<ol className="list-decimal list-inside space-y-1 text-sm">
<li>Copy the Webhook URL above</li>
<li>Configure your service to send HTTP POST requests to this URL</li>
{requireAuth && (
<>
<li>
{secretHeaderName
? `Add the "${secretHeaderName}" header with your token to all requests`
: 'Add an "Authorization: Bearer YOUR_TOKEN" header to all requests'}
</li>
</>
)}
</ol>
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
<p className="text-sm text-gray-700 dark:text-gray-300 flex items-center">
<span className="text-gray-400 dark:text-gray-500 mr-2">💡</span>
The webhook will receive all HTTP POST requests and pass the data to your
workflow.
</p>
</div>
</div>
{testResult && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={`p-3 rounded-md ${
testResult.success
? 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300 border border-green-200 dark:border-green-800'
: 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300 border border-red-200 dark:border-red-800'
}`}
>
<p className="text-sm">{testResult.message}</p>
</motion.div>
)}
</div>
<GenericConfig
requireAuth={requireAuth}
setRequireAuth={setRequireAuth}
generalToken={generalToken}
setGeneralToken={setGeneralToken}
secretHeaderName={secretHeaderName}
setSecretHeaderName={setSecretHeaderName}
allowedIps={allowedIps}
setAllowedIps={setAllowedIps}
isLoadingToken={isLoadingToken}
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
testWebhook={testWebhook}
/>
)
}
}
@@ -860,153 +415,47 @@ export function WebhookModal({
<>
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader className="flex flex-row items-center justify-between">
<div className="flex items-center">
<div className="mr-3 flex items-center">{getProviderIcon()}</div>
<div>
<DialogTitle>{getProviderTitle()}</DialogTitle>
<DialogDescription>
{webhookProvider === 'generic'
? 'Configure your webhook integration'
: `Configure your ${provider.name.toLowerCase()} integration`}
</DialogDescription>
</div>
</div>
<div className="flex items-center space-x-2">
{webhookId && (
<Badge
variant="outline"
className="bg-green-50 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-800"
>
Connected
</Badge>
)}
</div>
</DialogHeader>
<WebhookDialogHeader webhookProvider={webhookProvider} webhookId={webhookId} />
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label htmlFor="webhook-url">Webhook URL</Label>
<div className="flex items-center space-x-2">
{isLoadingToken ? (
<div className="flex-1 h-10 px-3 py-2 rounded-md border border-input bg-background flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
) : (
<Input id="webhook-url" value={webhookUrl} readOnly className="flex-1" />
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={() => copyToClipboard(webhookUrl, 'url')}
disabled={isLoadingToken}
>
{copied === 'url' ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<WebhookUrlField
webhookUrl={webhookUrl}
isLoadingToken={isLoadingToken}
copied={copied}
copyToClipboard={copyToClipboard}
/>
{renderProviderContent()}
</div>
<DialogFooter className="flex justify-between mt-6 sticky bottom-0 py-3 bg-background border-t z-10">
<div>
{webhookId && (
<div className="flex space-x-3">
<Button
variant="destructive"
onClick={() => setShowDeleteConfirm(true)}
disabled={isDeleting || isLoadingToken}
>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</>
)}
</Button>
{(webhookProvider === 'whatsapp' || webhookProvider === 'generic') && (
<Button
variant="outline"
onClick={testWebhook}
disabled={isTesting || isLoadingToken}
>
{isTesting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Testing...
</>
) : (
'Test'
)}
</Button>
)}
</div>
)}
</div>
<div className="flex space-x-3">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
variant="default"
onClick={handleSave}
disabled={isSaving || isLoadingToken}
className="bg-primary"
>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
'Save'
)}
</Button>
</div>
</DialogFooter>
<WebhookDialogFooter
webhookId={webhookId}
webhookProvider={webhookProvider}
isSaving={isSaving}
isDeleting={isDeleting}
isLoadingToken={isLoadingToken}
isTesting={isTesting}
onSave={handleSave}
onDelete={() => setShowDeleteConfirm(true)}
onTest={testWebhook}
onClose={handleClose}
/>
</DialogContent>
</Dialog>
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This will delete the webhook configuration. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700">
{isDeleting ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<DeleteConfirmDialog
open={showDeleteConfirm}
setOpen={setShowDeleteConfirm}
onConfirm={handleDelete}
isDeleting={isDeleting}
/>
<AlertDialog open={showUnsavedChangesConfirm} onOpenChange={setShowUnsavedChangesConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
<AlertDialogDescription>
You have unsaved changes. Are you sure you want to close without saving?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={handleCancelClose}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirmClose} className="bg-red-600 hover:bg-red-700">
Discard changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<UnsavedChangesDialog
open={showUnsavedChangesConfirm}
setOpen={setShowUnsavedChangesConfirm}
onCancel={handleCancelClose}
onConfirm={handleConfirmClose}
/>
</>
)
}