import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Card } from '../components/Card'; import { Badge } from '../components/Badge'; import { useConfig, wpApiFetch, externalApiFetch, isDevMode, PLAN_OVERRIDE_KEY, dispatchPlanChange } from '../hooks/useApi'; import { PLAN_LIMITS } from '@cra-compliance/types'; import type { DashboardStats, UserWebhook, WebhookEventType } from '@cra-compliance/types'; export function Settings() { const config = useConfig(); const isPremium = config.plan !== 'free'; const [savedApiKey, setSavedApiKey] = useState(config.apiKey || ''); const statsQuery = useQuery({ queryKey: ['dashboard'], enabled: isPremium, queryFn: async () => { const res = await externalApiFetch<{ data: DashboardStats }>('/api/dashboard'); return res.data; }, staleTime: 5 * 60 * 1000, }); const [apiKeyInput, setApiKeyInput] = useState(config.apiKey || ''); const [verifying, setVerifying] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [editingKey, setEditingKey] = useState(!config.apiKey); const maskedKey = savedApiKey ? `cra_************************${savedApiKey.slice(-8)}` : null; // Fetch fresh settings from DB on mount (config.apiKey can be stale from wp_localize_script) useEffect(() => { wpApiFetch<{ success: boolean; data: { api_key: string; plan: string } }>('settings') .then((res) => { if (!res.success) return; const serverKey = res.data.api_key || ''; setSavedApiKey(serverKey); setApiKeyInput(serverKey); setEditingKey(!serverKey); }) .catch(() => {}); }, []); const verifyAndSave = async () => { const nextKey = apiKeyInput.trim(); if (!nextKey) { setMessage({ type: 'error', text: 'Please enter an API key' }); return; } setVerifying(true); setMessage(null); try { const res = await wpApiFetch<{ success: boolean; data?: { plan: string }; error?: string }>( 'verify-key', { method: 'POST', body: JSON.stringify({ api_key: nextKey }), }, ); if (res.success && res.data) { setSavedApiKey(nextKey); setApiKeyInput(nextKey); setEditingKey(false); setMessage({ type: 'success', text: `API key verified! Plan: ${res.data.plan.toUpperCase()}. Reloading...`, }); // Reload so the wp_localize_script data refreshes with new plan setTimeout(() => window.location.reload(), 1000); } else { setEditingKey(true); setMessage({ type: 'error', text: res.error || 'Invalid API key' }); } } catch (err) { setEditingKey(true); setMessage({ type: 'error', text: err instanceof Error ? err.message : 'Failed to verify API key', }); } finally { setVerifying(false); } }; const clearApiKey = async () => { try { await wpApiFetch('settings', { method: 'POST', body: JSON.stringify({ api_key: '' }), }); setSavedApiKey(''); setApiKeyInput(''); setEditingKey(true); setMessage({ type: 'success', text: 'API key removed. Reloading...' }); // Reload so the wp_localize_script data refreshes (plan resets to free server-side) setTimeout(() => window.location.reload(), 1000); } catch { setMessage({ type: 'error', text: 'Failed to clear API key' }); } }; const currentLimits = PLAN_LIMITS[config.plan as keyof typeof PLAN_LIMITS] || PLAN_LIMITS.free; const stats = statsQuery.data ?? null; return (

Settings

Configure your ResilienceWP settings.

{config.plan.toUpperCase()} {config.plan === 'free' && (

Upgrade to Basic ($49/year) or Pro ($99/year) to unlock vulnerability scanning, email alerts, and compliance reports.{' '} View pricing →

)} {config.plan !== 'free' && (

Manage your subscription at{' '} resiliencewp.com/settings

)}
Monitored Plugins {currentLimits.max_plugins}
Scan Frequency {currentLimits.scan_frequency}
Scans Per Month {currentLimits.max_scans_per_month}
Compliance Reports {currentLimits.reports ? 'Yes' : 'No'}
Email Alerts {currentLimits.email_alerts ? 'Yes' : 'No'}
Priority Support {currentLimits.priority_support ? 'Yes' : 'No'}
{isPremium && stats && (
Last Scan {stats.last_scan_at ? new Date(stats.last_scan_at).toLocaleString() : 'No scans yet'}
Next Scan {stats.next_scan_at ? new Date(stats.next_scan_at).toLocaleString() : '—'}
Scans This Month {stats.scans_this_month ?? 0} / {currentLimits.max_scans_per_month}
Email Alerts {currentLimits.email_alerts ? 'Active' : 'Not available on your plan'}
Monitored Plugins {stats.total_plugins ?? 0} / {currentLimits.max_plugins}
)}
{maskedKey && !editingKey ? (
{maskedKey}
) : (
setApiKeyInput(e.target.value)} placeholder="cra_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" className="cra-input" style={{ flex: 1 }} /> {savedApiKey && editingKey && ( )}
)}

You'll receive your API key via email after purchasing a plan.

{message && (
{message.text}
)} {savedApiKey && !editingKey && ( )}
{config.plan === 'pro' && ( )}

ResilienceWP v{config.version}

Helping WordPress plugin developers comply with the EU Cyber Resilience Act.

This plugin is not legal advice. Consult with a legal professional for compliance questions specific to your situation.

{config.plan !== 'pro' && config.plan !== 'free' && (

Webhook integrations are available on the Pro plan. Receive HTTP callbacks when scans complete or vulnerabilities are found — ideal for CI/CD pipelines and external monitoring.{' '} Upgrade to Pro →

)} {isDevMode() && (

Switch between plans to test how the plugin behaves on each tier.

{(['free', 'basic', 'pro'] as const).map((plan) => { const currentOverride = localStorage.getItem(PLAN_OVERRIDE_KEY); const isActive = currentOverride === plan; return ( ); })} {localStorage.getItem(PLAN_OVERRIDE_KEY) && ( )}
)}
); } const ALL_EVENTS: { value: WebhookEventType; label: string }[] = [ { value: 'scan.completed', label: 'Scan completed' }, { value: 'vulnerability.found', label: 'Vulnerability found' }, ]; function WebhookSettings() { const [showForm, setShowForm] = useState(false); const [url, setUrl] = useState(''); const [secret, setSecret] = useState(''); const [events, setEvents] = useState(['scan.completed', 'vulnerability.found']); const [error, setError] = useState(null); const { data: webhooks = [], refetch } = useQuery({ queryKey: ['webhooks'], queryFn: async () => { const res = await externalApiFetch<{ success: boolean; data: UserWebhook[] }>('/api/webhooks'); return res.data ?? []; }, staleTime: 30 * 1000, retry: 1, }); const createMutation = useMutation({ mutationFn: async () => { const res = await externalApiFetch<{ data: UserWebhook; error?: string }>('/api/webhooks', { method: 'POST', body: JSON.stringify({ url, secret: secret || undefined, events }), }); if (!res.data && (res as { error?: string }).error) { throw new Error((res as { error?: string }).error); } return res.data; }, onSuccess: () => { void refetch(); setShowForm(false); setUrl(''); setSecret(''); setEvents(['scan.completed', 'vulnerability.found']); setError(null); }, onError: (err: Error) => setError(err.message), }); const toggleMutation = useMutation({ mutationFn: async ({ id, active }: { id: string; active: boolean }) => { await externalApiFetch('/api/webhooks', { method: 'PATCH', body: JSON.stringify({ id, active }), }); }, onSuccess: () => void refetch(), }); const deleteMutation = useMutation({ mutationFn: async (id: string) => { await externalApiFetch(`/api/webhooks?id=${id}`, { method: 'DELETE' }); }, onSuccess: () => void refetch(), }); const testMutation = useMutation({ mutationFn: async () => { await externalApiFetch('/api/dashboard', { method: 'POST', body: JSON.stringify({ action: 'webhook_test' }) }); }, }); const toggleEvent = (event: WebhookEventType) => { setEvents((prev) => prev.includes(event) ? prev.filter((e) => e !== event) : [...prev, event], ); }; return ( {webhooks.length > 0 && (
{webhooks.map((wh) => (
{wh.url}
{wh.events.join(', ')}
))}
)} {!showForm ? (
{webhooks.length > 0 && ( )}
) : (
setUrl(e.target.value)} placeholder="https://example.com/webhook" />
setSecret(e.target.value)} placeholder="Your HMAC signing secret" />

If set, each delivery includes an X-Webhook-Signature header (HMAC-SHA256).

{ALL_EVENTS.map((evt) => ( ))}
{error &&
{error}
}
)}
Payload structure

Each delivery is an HTTP POST with Content-Type: application/json. If a signing secret is set, the X-Webhook-Signature header contains sha256=<hmac>.

scan.completed

{`{
  "event": "scan.completed",
  "timestamp": "2026-03-10T12:00:00.000Z",
  "data": {
    "scan_id": "uuid",
    "plugin_slug": "my-plugin",
    "plugin_name": "My Plugin",
    "vulnerabilities_found": 2,
    "new_vulnerabilities": 1,
    "status": "vulnerable"
  }
}`}

vulnerability.found

{`{
  "event": "vulnerability.found",
  "timestamp": "2026-03-10T12:00:00.000Z",
  "data": {
    "plugin_slug": "my-plugin",
    "plugin_name": "My Plugin",
    "vulnerabilities": [
      {
        "title": "SQL Injection in REST endpoint",
        "severity": "critical",
        "cve_id": "CVE-2026-12345",
        "fixed_in_version": "2.1.0"
      }
    ]
  }
}`}

Test endpoint: point a webhook URL to{' '} https://api.resiliencewp.com/api/dashboard{' '} to inspect delivered payloads.

); }