'use client' import { useState } from 'react' import { Check, Eye, EyeOff } from 'lucide-react' import { saveIntegration, disconnectIntegration } from '@/lib/actions' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import type { IntegrationConfig, Integration, SaveStatus } from '../types' const INTEGRATIONS_CONFIG: IntegrationConfig[] = [ { platform: 'paypal', name: 'PayPal', description: 'Sync transactions for member dues', icon: ( ), fields: [ { key: 'client_id', label: 'Client ID', type: 'text', placeholder: 'Sandbox or Live Client ID' }, { key: 'client_secret', label: 'Client Secret', type: 'password', placeholder: 'Client Secret from PayPal Developer' }, { key: 'mode', label: 'Mode', type: 'select', options: ['sandbox', 'live'] }, ], docs: 'https://developer.paypal.com/api/rest/', }, { platform: 'zeffy', name: 'Zeffy', description: 'Nonprofit payment and donation sync', icon: ( ), fields: [ { key: 'api_key', label: 'API Key', type: 'password', placeholder: 'Zeffy API Key' }, { key: 'organization_id', label: 'Organization ID', type: 'text', placeholder: 'Your Zeffy Org ID' }, ], docs: 'https://docs.zeffy.com/', }, { platform: 'venmo', name: 'Venmo', description: 'Pull Venmo business transactions', icon: ( ), fields: [ { key: 'access_token', label: 'Access Token', type: 'password', placeholder: 'Venmo API Access Token' }, ], docs: 'https://developer.venmo.com/', }, // Stripe is intentionally NOT listed here. Stripe recurring dues is // configured in the dedicated Dues tab (saveStripeSettings: mode, tier->price // map, grace days, vaulted keys). The generic saveIntegration path OVERWRITES // integrations.config with a different shape, which would clobber the dues // config (drop prices/mode/grace_days). One Stripe config UI, in Dues. ] interface Props extends SaveStatus { integrations: Integration[] isAdmin: boolean } export function IntegrationsPanel({ integrations, isAdmin, saving, setSaving, setMessage }: Props) { const [editingIntegration, setEditingIntegration] = useState(null) const [integrationForm, setIntegrationForm] = useState>({}) const [showSecrets, setShowSecrets] = useState>({}) async function handleSaveIntegration(e: React.FormEvent) { e.preventDefault() if (!editingIntegration || !isAdmin) return setSaving(true) setMessage(null) const result = await saveIntegration(editingIntegration.platform, integrationForm) if (result.error) { setMessage({ type: 'error', text: result.error }) } else { setMessage({ type: 'success', text: `${editingIntegration.name} connected!` }) setEditingIntegration(null) } setSaving(false) } async function handleDisconnect(platform: string) { if (!isAdmin) return setSaving(true) await disconnectIntegration(platform) setSaving(false) } function openIntegrationModal(config: IntegrationConfig) { const existing = integrations.find(i => i.platform === config.platform) const initialForm: Record = {} config.fields.forEach(f => { initialForm[f.key] = (existing?.config as Record | undefined)?.[f.key] || '' }) setIntegrationForm(initialForm) setEditingIntegration(config) } const getIntegrationStatus = (platform: string) => integrations.find(i => i.platform === platform)?.is_connected ?? false return ( <>

Integrations

{INTEGRATIONS_CONFIG.map(config => { const connected = getIntegrationStatus(config.platform) return (
{config.icon}

{config.name}

{config.description}

{connected ? ( <> Connected {isAdmin && ( )} ) : isAdmin ? ( ) : ( Not connected )}
) })}
{/* Integration Config Modal */} { if (!o) setEditingIntegration(null) }}> {editingIntegration && ( <>
{editingIntegration.icon}
{editingIntegration.name}
{editingIntegration.fields.map(field => (
{field.type === 'select' ? ( ) : (
setIntegrationForm(f => ({ ...f, [field.key]: e.target.value }))} placeholder={field.placeholder} className="w-full bg-background border border-border rounded px-3 py-2 pr-10 font-mono text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-primary" /> {field.type === 'password' && ( )}
)}
))}
{getIntegrationStatus(editingIntegration.platform) && ( )}
)}
) }