mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-29 18:47:25 +08:00
feat(webhooks): testing whatsapp webhook in prod
This commit is contained in:
@@ -115,6 +115,106 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// Add GET method to handle verification for providers like WhatsApp
|
// Add GET method to handle verification for providers like WhatsApp
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
// Reuse the same logic for verification
|
try {
|
||||||
return POST(request)
|
// Get the webhook path from the URL
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const path = url.pathname.replace('/api/webhooks/trigger', '')
|
||||||
|
|
||||||
|
if (!path || path === '/') {
|
||||||
|
return new NextResponse('Invalid webhook path', { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the webhook in the database
|
||||||
|
const webhooks = await db
|
||||||
|
.select({
|
||||||
|
webhook: webhook,
|
||||||
|
workflow: workflow,
|
||||||
|
})
|
||||||
|
.from(webhook)
|
||||||
|
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
|
||||||
|
.where(and(eq(webhook.path, path), eq(webhook.isActive, true), eq(workflow.isDeployed, true)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
return new NextResponse('Webhook not found', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { webhook: foundWebhook, workflow: foundWorkflow } = webhooks[0]
|
||||||
|
|
||||||
|
// Handle provider-specific verification and authentication
|
||||||
|
if (foundWebhook.provider) {
|
||||||
|
const authHeader = request.headers.get('authorization')
|
||||||
|
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
|
||||||
|
|
||||||
|
switch (foundWebhook.provider) {
|
||||||
|
case 'whatsapp':
|
||||||
|
// Handle WhatsApp verification
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
const mode = url.searchParams.get('hub.mode')
|
||||||
|
const token = url.searchParams.get('hub.verify_token')
|
||||||
|
const challenge = url.searchParams.get('hub.challenge')
|
||||||
|
|
||||||
|
if (mode === 'subscribe' && token) {
|
||||||
|
if (token === providerConfig.verificationToken) {
|
||||||
|
return new NextResponse(challenge, { status: 200 })
|
||||||
|
} else {
|
||||||
|
return new NextResponse('Verification token mismatch', { status: 403 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'github':
|
||||||
|
// GitHub doesn't require verification in this implementation
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'stripe':
|
||||||
|
// Stripe verification would go here if needed
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
// For generic webhooks, check for a token if provided in providerConfig
|
||||||
|
if (providerConfig.token) {
|
||||||
|
const providedToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null
|
||||||
|
if (!providedToken || providedToken !== providerConfig.token) {
|
||||||
|
return new NextResponse('Unauthorized', { status: 401 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the request body
|
||||||
|
const body = await request.json().catch(() => ({}))
|
||||||
|
|
||||||
|
// Create execution context with the webhook payload
|
||||||
|
const executionId = nanoid()
|
||||||
|
|
||||||
|
// Format the input to match the expected BlockOutput type
|
||||||
|
const input = {
|
||||||
|
webhook: {
|
||||||
|
data: {
|
||||||
|
path,
|
||||||
|
provider: foundWebhook.provider,
|
||||||
|
providerConfig: foundWebhook.providerConfig,
|
||||||
|
payload: body,
|
||||||
|
headers: Object.fromEntries(request.headers.entries()),
|
||||||
|
method: request.method,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the workflow
|
||||||
|
if (foundWorkflow.state) {
|
||||||
|
const executor = new Executor(foundWorkflow.state as SerializedWorkflow, input)
|
||||||
|
const result = await executor.execute(foundWorkflow.id)
|
||||||
|
|
||||||
|
// Return the execution result
|
||||||
|
return NextResponse.json(result, { status: 200 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse('Workflow state not found', { status: 500 })
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error processing webhook:', error)
|
||||||
|
return new NextResponse(`Internal Server Error: ${error.message}`, { status: 500 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export async function GET(request: NextRequest) {
|
|||||||
const verificationToken = providerConfig.verificationToken
|
const verificationToken = providerConfig.verificationToken
|
||||||
|
|
||||||
if (verificationToken && token === verificationToken) {
|
if (verificationToken && token === verificationToken) {
|
||||||
console.log('WhatsApp webhook verified')
|
|
||||||
return new NextResponse(challenge, { status: 200 })
|
return new NextResponse(challenge, { status: 200 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-6
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { ExternalLink } from 'lucide-react'
|
import { CheckCircle2, ExternalLink } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { WebhookModal } from '@/components/ui/webhook-modal'
|
import { WebhookModal } from '@/components/ui/webhook-modal'
|
||||||
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
import { useSubBlockValue } from '../hooks/use-sub-block-value'
|
||||||
@@ -29,7 +29,9 @@ interface WebhookConfigProps {
|
|||||||
export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConfigProps) {
|
export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConfigProps) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [webhookId, setWebhookId] = useState<string | null>(null)
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workflowId = params.id as string
|
const workflowId = params.id as string
|
||||||
|
|
||||||
@@ -37,12 +39,35 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
|
|||||||
const [webhookProvider] = useSubBlockValue(blockId, 'webhookProvider')
|
const [webhookProvider] = useSubBlockValue(blockId, 'webhookProvider')
|
||||||
|
|
||||||
// Generate a default path based on the workflow ID if none exists
|
// Generate a default path based on the workflow ID if none exists
|
||||||
const defaultPath = `/${workflowId.substring(0, 8)}`
|
const defaultPath = `${workflowId.substring(0, 8)}`
|
||||||
// Use the default path if no path is set
|
// Use the default path if no path is set
|
||||||
const [webhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath')
|
const [webhookPath, setWebhookPath] = useSubBlockValue(blockId, 'webhookPath')
|
||||||
// Store provider-specific configuration
|
// Store provider-specific configuration
|
||||||
const [providerConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig')
|
const [providerConfig, setProviderConfig] = useSubBlockValue(blockId, 'providerConfig')
|
||||||
|
|
||||||
|
// Check if webhook exists in the database
|
||||||
|
useEffect(() => {
|
||||||
|
const checkWebhook = async () => {
|
||||||
|
try {
|
||||||
|
// Check if there's a webhook for this workflow with this path
|
||||||
|
const pathToCheck = webhookPath || defaultPath
|
||||||
|
const response = await fetch(`/api/webhooks?workflowId=${workflowId}&path=${pathToCheck}`)
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.webhooks && data.webhooks.length > 0) {
|
||||||
|
setWebhookId(data.webhooks[0].webhook.id)
|
||||||
|
} else {
|
||||||
|
setWebhookId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking webhook:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWebhook()
|
||||||
|
}, [webhookPath, workflowId, defaultPath])
|
||||||
|
|
||||||
const handleOpenModal = () => {
|
const handleOpenModal = () => {
|
||||||
setIsModalOpen(true)
|
setIsModalOpen(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -84,6 +109,9 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
|
|||||||
throw new Error(errorData.error || 'Failed to save webhook')
|
throw new Error(errorData.error || 'Failed to save webhook')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
setWebhookId(data.webhook.id)
|
||||||
|
|
||||||
console.log('Webhook configuration saved successfully')
|
console.log('Webhook configuration saved successfully')
|
||||||
return true
|
return true
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -95,6 +123,36 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteWebhook = async () => {
|
||||||
|
if (!webhookId) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsDeleting(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/webhooks/${webhookId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.error || 'Failed to delete webhook')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the webhook ID
|
||||||
|
setWebhookId(null)
|
||||||
|
|
||||||
|
console.log('Webhook deleted successfully')
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error deleting webhook:', error)
|
||||||
|
setError(error.message || 'Failed to delete webhook')
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{error && <div className="text-sm text-red-500 mb-2">{error}</div>}
|
{error && <div className="text-sm text-red-500 mb-2">{error}</div>}
|
||||||
@@ -103,10 +161,19 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleOpenModal}
|
onClick={handleOpenModal}
|
||||||
disabled={isConnecting || isSaving}
|
disabled={isConnecting || isSaving || isDeleting}
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-4 w-4 mr-2" />
|
{webhookId ? (
|
||||||
{isSaving ? 'Saving...' : 'View Webhook URL'}
|
<>
|
||||||
|
<CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
|
||||||
|
{isSaving ? 'Saving...' : isDeleting ? 'Deleting...' : 'Webhook Connected'}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||||||
|
{isSaving ? 'Saving...' : isDeleting ? 'Deleting...' : 'Configure Webhook'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
@@ -117,6 +184,8 @@ export function WebhookConfig({ blockId, subBlockId, isConnecting }: WebhookConf
|
|||||||
webhookProvider={webhookProvider || 'generic'}
|
webhookProvider={webhookProvider || 'generic'}
|
||||||
workflowId={workflowId}
|
workflowId={workflowId}
|
||||||
onSave={handleSaveWebhook}
|
onSave={handleSaveWebhook}
|
||||||
|
onDelete={handleDeleteWebhook}
|
||||||
|
webhookId={webhookId || undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+237
-76
@@ -1,6 +1,17 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Check, Copy, X } from 'lucide-react'
|
import { Check, Copy, Loader2, Trash2, X } from 'lucide-react'
|
||||||
import { GithubIcon, StripeIcon, WhatsAppIcon } from '@/components/icons'
|
import { GithubIcon, StripeIcon, WhatsAppIcon } from '@/components/icons'
|
||||||
|
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 { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -35,6 +46,8 @@ interface WebhookModalProps {
|
|||||||
webhookProvider: string
|
webhookProvider: string
|
||||||
workflowId: string
|
workflowId: string
|
||||||
onSave?: (path: string, providerConfig: ProviderConfig) => void
|
onSave?: (path: string, providerConfig: ProviderConfig) => void
|
||||||
|
onDelete?: () => void
|
||||||
|
webhookId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WebhookModal({
|
export function WebhookModal({
|
||||||
@@ -44,14 +57,48 @@ export function WebhookModal({
|
|||||||
webhookProvider,
|
webhookProvider,
|
||||||
workflowId,
|
workflowId,
|
||||||
onSave,
|
onSave,
|
||||||
|
onDelete,
|
||||||
|
webhookId,
|
||||||
}: WebhookModalProps) {
|
}: WebhookModalProps) {
|
||||||
const [copied, setCopied] = useState<string | null>(null)
|
const [copied, setCopied] = useState<string | null>(null)
|
||||||
const [saving, setSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
|
const isConfigured = Boolean(webhookId)
|
||||||
|
|
||||||
// Provider-specific configuration state
|
// Provider-specific configuration state
|
||||||
const [whatsappVerificationToken, setWhatsappVerificationToken] = useState('')
|
const [whatsappVerificationToken, setWhatsappVerificationToken] = useState('')
|
||||||
const [githubContentType, setGithubContentType] = useState('application/json')
|
const [githubContentType, setGithubContentType] = useState('application/json')
|
||||||
|
|
||||||
|
// Load existing configuration values
|
||||||
|
useEffect(() => {
|
||||||
|
if (webhookId) {
|
||||||
|
// If we have a webhook ID, try to fetch the existing configuration
|
||||||
|
const fetchWebhookConfig = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/webhooks/${webhookId}`)
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.webhook?.webhook?.providerConfig) {
|
||||||
|
const config = data.webhook.webhook.providerConfig
|
||||||
|
|
||||||
|
// Check provider type and set appropriate state
|
||||||
|
if (webhookProvider === 'whatsapp' && 'verificationToken' in config) {
|
||||||
|
setWhatsappVerificationToken(config.verificationToken)
|
||||||
|
} else if (webhookProvider === 'github' && 'contentType' in config) {
|
||||||
|
setGithubContentType(config.contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching webhook config:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchWebhookConfig()
|
||||||
|
}
|
||||||
|
}, [webhookId, webhookProvider])
|
||||||
|
|
||||||
// Format the path to ensure it starts with a slash
|
// Format the path to ensure it starts with a slash
|
||||||
const formattedPath =
|
const formattedPath =
|
||||||
webhookPath && webhookPath.trim() !== ''
|
webhookPath && webhookPath.trim() !== ''
|
||||||
@@ -88,19 +135,35 @@ export function WebhookModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (onSave) {
|
setIsSaving(true)
|
||||||
setSaving(true)
|
try {
|
||||||
try {
|
// Call the onSave callback with the path and provider-specific config
|
||||||
|
if (onSave) {
|
||||||
const providerConfig = getProviderConfig()
|
const providerConfig = getProviderConfig()
|
||||||
await onSave(webhookPath || formattedPath.substring(1), providerConfig)
|
// Use the path without the leading slash
|
||||||
onClose()
|
const pathToSave = formattedPath.startsWith('/')
|
||||||
} catch (error) {
|
? formattedPath.substring(1)
|
||||||
console.error('Error saving webhook configuration:', error)
|
: formattedPath
|
||||||
} finally {
|
await onSave(pathToSave, providerConfig)
|
||||||
setSaving(false)
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
onClose()
|
console.error('Error saving webhook:', error)
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsDeleting(true)
|
||||||
|
try {
|
||||||
|
if (onDelete) {
|
||||||
|
await onDelete()
|
||||||
|
setShowDeleteConfirm(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting webhook:', error)
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,15 +203,59 @@ export function WebhookModal({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-medium">Setup Instructions</h4>
|
<h4 className="font-medium">Setup Instructions</h4>
|
||||||
<ol className="list-decimal list-inside space-y-1 text-sm">
|
<ol className="space-y-2">
|
||||||
<li>Go to your Meta for Developers dashboard</li>
|
<li className="flex items-start">
|
||||||
<li>Navigate to your WhatsApp app settings</li>
|
<span className="text-gray-500 mr-2">1.</span>
|
||||||
<li>Under "Webhooks", click "Configure"</li>
|
<span className="text-sm">Go to WhatsApp Business Platform dashboard</span>
|
||||||
<li>Enter the Webhook URL shown above</li>
|
</li>
|
||||||
<li>Enter the verification token you specified above</li>
|
<li className="flex items-start">
|
||||||
<li>Subscribe to the "messages" webhook field</li>
|
<span className="text-gray-500 mr-2">2.</span>
|
||||||
<li>Save your changes</li>
|
<span className="text-sm">Navigate to "Configuration" in the sidebar</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-gray-500 mr-2">3.</span>
|
||||||
|
<span className="text-sm">Enter the URL above as "Callback URL"</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-gray-500 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 mr-2">5.</span>
|
||||||
|
<span className="text-sm">
|
||||||
|
Click "Verify and save" and subscribe to "messages"
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<div className="bg-blue-50 p-3 rounded-md mt-3">
|
||||||
|
<h5 className="text-sm font-medium text-blue-800">Requirements</h5>
|
||||||
|
<ul className="mt-1 space-y-1">
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-blue-500 mr-2">•</span>
|
||||||
|
<span className="text-sm text-blue-700">
|
||||||
|
URL must be publicly accessible with HTTPS
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-blue-500 mr-2">•</span>
|
||||||
|
<span className="text-sm text-blue-700">
|
||||||
|
Self-signed SSL certificates not supported
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="text-blue-500 mr-2">•</span>
|
||||||
|
<span className="text-sm text-blue-700">
|
||||||
|
For local testing, use ngrok to expose your server
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 p-3 rounded-md mt-3">
|
||||||
|
<p className="text-sm text-gray-700 flex items-center">
|
||||||
|
<span className="text-gray-400 mr-2">💡</span>
|
||||||
|
After saving, use "Test" to verify your webhook connection.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -205,64 +312,118 @@ export function WebhookModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
<>
|
||||||
<DialogContent className="sm:max-w-md">
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
<DialogHeader className="flex flex-row items-center gap-2">
|
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
{getProviderIcon()}
|
<DialogHeader className="flex flex-row items-start">
|
||||||
<div>
|
<div className="mr-3">{getProviderIcon()}</div>
|
||||||
<DialogTitle>Webhook Configuration</DialogTitle>
|
<div>
|
||||||
<DialogDescription>
|
<DialogTitle>Webhook Configuration</DialogTitle>
|
||||||
Configure your{' '}
|
<DialogDescription>Configure your WhatsApp integration</DialogDescription>
|
||||||
{webhookProvider === 'whatsapp'
|
</div>
|
||||||
? 'WhatsApp'
|
{webhookId && (
|
||||||
: webhookProvider === 'github'
|
<Badge
|
||||||
? 'GitHub'
|
|
||||||
: webhookProvider === 'stripe'
|
|
||||||
? 'Stripe'
|
|
||||||
: 'webhook'}{' '}
|
|
||||||
integration
|
|
||||||
</DialogDescription>
|
|
||||||
</div>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="webhook-url">Webhook URL</Label>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Input id="webhook-url" value={webhookUrl} readOnly className="flex-1" />
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
className="ml-auto bg-green-50 text-green-700 border-green-200"
|
||||||
onClick={() => copyToClipboard(webhookUrl, 'url')}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
title="Copy URL"
|
|
||||||
>
|
>
|
||||||
{copied === 'url' ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
Connected
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<Input id="webhook-url" value={webhookUrl} readOnly className="flex-1" />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => copyToClipboard(webhookUrl, 'url')}
|
||||||
|
>
|
||||||
|
{copied === 'url' ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderProviderContent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-y-0 space-y-2 gap-2 mt-4">
|
||||||
|
{webhookId && (
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto sm:mr-auto">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isDeleting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
||||||
|
Deleting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Trash2 className="mr-2 h-3 w-3" />
|
||||||
|
Delete
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{webhookProvider === 'whatsapp' && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => window.open(`/webhooks/test/${webhookId}`, '_blank')}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||||
|
<Button variant="outline" onClick={onClose} className="w-full sm:w-auto" size="sm">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-3 w-3 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Save'
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Provider-specific instructions and configuration */}
|
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||||
<div className="space-y-2 pt-2 border-t">{renderProviderContent()}</div>
|
<AlertDialogContent>
|
||||||
</div>
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
<DialogFooter className="sm:justify-between">
|
<AlertDialogDescription>
|
||||||
<Button variant="outline" onClick={onClose}>
|
This will delete the webhook configuration. This action cannot be undone.
|
||||||
Cancel
|
</AlertDialogDescription>
|
||||||
</Button>
|
</AlertDialogHeader>
|
||||||
<div className="flex space-x-2">
|
<AlertDialogFooter>
|
||||||
<Button
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
variant="secondary"
|
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700">
|
||||||
onClick={() => window.open(`/api/webhooks/${workflowId}/test`, '_blank')}
|
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||||
>
|
</AlertDialogAction>
|
||||||
Test Webhook
|
</AlertDialogFooter>
|
||||||
</Button>
|
</AlertDialogContent>
|
||||||
<Button onClick={handleSave} disabled={saving}>
|
</AlertDialog>
|
||||||
{saving ? 'Saving...' : 'Save Configuration'}
|
</>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user