'use client'; import { useEffect, useId, useState, useSyncExternalStore } from 'react'; import { useTranslations } from 'next-intl'; import { KeyRound } from '@/components/icons'; import { Button } from '@/components/ui/button'; import { useConfig } from '@/hooks/use-config'; import { useFocusTrap } from '@/hooks/use-focus-trap'; import { productName } from '@/lib/product-name'; import { currentConnect, originFromInput, saveCredential, subscribeConnect, } from '@/lib/plugin-sandbox/connect-dialog'; /** * The host's own "Connect account" dialog (see lib/plugin-sandbox/connect-dialog.ts). * Everything here is rendered by the host: the plugin can't change the text, * and what the user types goes to the server, never to the plugin. */ export function PluginConnectDialog() { const request = useSyncExternalStore(subscribeConnect, currentConnect, () => null); const t = useTranslations('plugins.connect'); const appName = productName(useConfig().appName); const titleId = useId(); const [server, setServer] = useState(''); const [username, setUsername] = useState(''); const [secret, setSecret] = useState(''); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); useEffect(() => { setServer(request?.suggestedOrigin ?? ''); setUsername(request?.suggestedUsername ?? ''); setSecret(''); setError(null); setSaving(false); }, [request?.id, request?.suggestedOrigin, request?.suggestedUsername]); useEffect(() => { if (!request) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') request.resolve({ connected: false }); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [request]); // Tab stays in the dialog, the first field takes the focus, and the focus // goes back to where it was when the dialog closes. (Escape is the // listener above.) const panelRef = useFocusTrap({ isActive: !!request, restoreFocus: true, initialFocus: () => panelRef.current?.querySelector('input:not(:disabled)'), }); if (!request) return null; const { spec, mode, pluginName } = request; const fixedOrigin = spec.origin; const needsUsername = mode === 'password' && spec.inject.type === 'basic'; const needsSecret = mode === 'password' && spec.inject.type !== 'none'; const origin = fixedOrigin ?? originFromInput(server); const canSubmit = !!origin && (!needsSecret || !!secret) && (!needsUsername || !!username.trim()) && !saving; const submit = async () => { if (!origin) { setError(t('error_server')); return; } setSaving(true); setError(null); const failure = await saveCredential({ pluginId: request.pluginId, credentialId: request.credentialId, label: spec.label, origin, inject: spec.inject, ...(needsUsername ? { username: username.trim() } : {}), ...(needsSecret ? { secret } : {}), }); setSaving(false); if (failure) { setError(t('error_save', { error: failure })); return; } request.resolve({ connected: mode === 'password', origin }); }; const host = origin ? new URL(origin).host : null; const intro = mode === 'signin' ? t('intro_signin', { name: pluginName, label: spec.label, appName }) : host ? t('intro_password', { name: pluginName, label: spec.label, host, appName }) : t('intro_password_any', { name: pluginName, label: spec.label, appName }); return (
{ if (e.target === e.currentTarget) request.resolve({ connected: false }); }} >
{ e.preventDefault(); void submit(); }}>

{intro}

{!fixedOrigin && ( )} {needsUsername && ( )} {needsSecret && ( )} {error &&

{error}

}
); }