import { useState, useCallback, useEffect } from 'react'; import { desktopHostsGet, desktopHostsSet, desktopHostProbe, resolveDesktopHostUrl, importDesktopHostPairing, type DesktopHost, type HostProbeResult, } from '@/lib/desktopHosts'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { isDesktopShell, restartDesktopApp } from '@/lib/desktop'; import { useI18n } from '@/lib/i18n'; type ConnectionState = 'idle' | 'testing' | 'success' | 'error'; export interface RemoteConnectionFormProps { onBack: () => void; /** Optional: show the back button (default: true) */ showBackButton?: boolean; /** Optional: initial URL to pre-populate */ initialUrl?: string; /** Optional: initial label to pre-populate */ initialLabel?: string; /** Optional: show recovery mode styling/behavior */ isRecoveryMode?: boolean; /** Optional: callback when successfully connected */ onConnect?: () => void; /** Optional: callback when user wants to switch to local setup */ onSwitchToLocal?: () => void; showInstancePicker?: boolean; } type ProbeStatus = HostProbeResult['status'] | null; function getProbeStatusMessageKey(status: ProbeStatus): string | null { switch (status) { case 'ok': return null; // Success is shown separately case 'auth': return 'onboarding.remoteConnection.probe.authMessage'; case 'update-recommended': return 'onboarding.remoteConnection.probe.updateRecommendedMessage'; case 'incompatible': return 'onboarding.remoteConnection.probe.incompatibleMessage'; case 'wrong-service': return 'onboarding.remoteConnection.probe.wrongServiceMessage'; case 'unreachable': return 'onboarding.remoteConnection.probe.unreachableMessage'; default: return null; } } function isBlockingStatus(status: ProbeStatus): boolean { return status === 'wrong-service' || status === 'unreachable' || status === 'incompatible'; } export function RemoteConnectionForm({ onBack, showBackButton = true, initialUrl = '', initialLabel = '', isRecoveryMode = false, onConnect, onSwitchToLocal, showInstancePicker = false, }: RemoteConnectionFormProps) { const { t } = useI18n(); const [url, setUrl] = useState(initialUrl); const [label, setLabel] = useState(initialLabel); const [state, setState] = useState('idle'); const [probeResult, setProbeResult] = useState(null); const [error, setError] = useState(''); const [hosts, setHosts] = useState([]); const [view, setView] = useState<'instances' | 'add' | 'import'>(() => showInstancePicker ? 'instances' : 'add'); const [connectLink, setConnectLink] = useState(''); useEffect(() => { if (!showInstancePicker) return; void desktopHostsGet().then((config) => setHosts(config.hosts)).catch((err) => { setError(err instanceof Error ? err.message : String(err)); }); }, [showInstancePicker]); const resolvedUrl = resolveDesktopHostUrl(url); const normalizedUrl = resolvedUrl?.persistedUrl ?? null; const handleUrlChange = useCallback((e: React.ChangeEvent) => { setUrl(e.target.value); setState('idle'); setProbeResult(null); setError(''); }, []); const handleLabelChange = useCallback((e: React.ChangeEvent) => { setLabel(e.target.value); }, []); const handleTest = useCallback(async () => { if (!normalizedUrl) return; setState('testing'); setProbeResult(null); setError(''); try { const result = await desktopHostProbe(normalizedUrl); setProbeResult(result); setState(result.status === 'ok' || result.status === 'update-recommended' ? 'success' : 'error'); } catch (err) { setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.connectionTestFailed')); setState('error'); } }, [normalizedUrl, t]); const handleConnect = useCallback(async () => { if (!resolvedUrl) return; const targetUrl = resolvedUrl.persistedUrl; setState('testing'); setProbeResult(null); setError(''); try { const probe = await desktopHostProbe(targetUrl); setProbeResult(probe); // Block connection on wrong-service or unreachable if (isBlockingStatus(probe.status)) { setState('error'); return; } const config = await desktopHostsGet(); const hostLabel = label.trim() || targetUrl; const existingHost = config.hosts.find( (h) => h.url === targetUrl ); const hostId = existingHost ? existingHost.id : `host-${Date.now().toString(16)}`; const newHost = { id: hostId, label: hostLabel, url: targetUrl, apiUrl: targetUrl, }; const updatedHosts = existingHost ? config.hosts.map((h) => (h.id === hostId ? newHost : h)) : [...config.hosts, newHost]; // Set as default and mark initial choice completed await desktopHostsSet({ hosts: updatedHosts, defaultHostId: hostId, initialHostChoiceCompleted: true, }); onConnect?.(); if (resolvedUrl.redeemUrl) { window.location.assign(resolvedUrl.redeemUrl); return; } if (isDesktopShell()) { await restartDesktopApp(); } } catch (err) { setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.failedToSaveConnection')); setState('error'); } }, [resolvedUrl, label, onConnect, t]); const selectHost = useCallback(async (hostId: string) => { const config = await desktopHostsGet(); await desktopHostsSet({ hosts: config.hosts, defaultHostId: hostId, initialHostChoiceCompleted: true, }); await restartDesktopApp(); }, []); const handleImport = useCallback(async () => { setState('testing'); setError(''); try { const config = await desktopHostsGet(); const imported = await importDesktopHostPairing(connectLink, config.hosts); await desktopHostsSet({ hosts: imported.hosts, defaultHostId: imported.hostId, initialHostChoiceCompleted: true, }); await restartDesktopApp(); } catch (err) { setError( err instanceof Error && err.message === 'invalid-connect-link' ? t('settings.remoteInstances.direct.error.invalidConnectLink') : t('onboarding.remoteConnection.errors.failedToSaveConnection'), ); setState('error'); } }, [connectLink, t]); const isTesting = state === 'testing'; const canTest = normalizedUrl !== null && !isTesting; const canConnect = normalizedUrl !== null && !isTesting && !isBlockingStatus(probeResult?.status ?? null); const probeMessageKey = getProbeStatusMessageKey(probeResult?.status ?? null); const isSuccess = probeResult?.status === 'ok'; const isUpdateRecommended = probeResult?.status === 'update-recommended'; const isAuth = probeResult?.status === 'auth'; const isBlocking = isBlockingStatus(probeResult?.status ?? null); if (showInstancePicker && view === 'instances') { return (

{t('desktopHostSwitcher.actions.switchInstance')}

{t('settings.remoteInstances.direct.description')}

{error ?
{error}
: null}
{hosts.length === 0 ? (
{t('settings.remoteInstances.direct.state.empty')}
) : hosts.map((host) => ( ))}
); } if (showInstancePicker && view === 'import') { return (

{t('settings.remoteInstances.direct.import.action')}

{t('settings.remoteInstances.direct.import.description')}

setConnectLink(event.target.value)} placeholder={t('settings.remoteInstances.direct.import.placeholder')} disabled={isTesting} autoFocus /> {error ?
{error}
: null}
); } return (
{(showBackButton || showInstancePicker) && (
)}

{isRecoveryMode ? t('onboarding.remoteConnection.titleRecovery') : t('onboarding.remoteConnection.title')}

{t('onboarding.remoteConnection.description')}

{/* Success message */} {probeResult && isSuccess && (
{t('onboarding.remoteConnection.status.connectedSuccessfully', { latencyMs: probeResult.latencyMs })}
)} {/* Auth warning (non-blocking) */} {probeResult && isAuth && (
{t('onboarding.remoteConnection.status.authWarning')}
)} {probeResult && isUpdateRecommended && (
{probeMessageKey ? t(probeMessageKey as Parameters[0]) : null}
)} {/* Blocking errors */} {probeResult && isBlocking && (
{t('onboarding.remoteConnection.status.connectionFailed')}
{probeMessageKey ? t(probeMessageKey as Parameters[0]) : null}
{probeResult.status === 'unreachable' ? t('onboarding.remoteConnection.status.suggestionsUnreachable') : t('onboarding.remoteConnection.status.suggestionsWrongService')}
)} {/* Generic error */} {error && (
{error}
)}
{/* Suggested actions when connection is blocked */} {isBlocking && (
{t('onboarding.remoteConnection.actions.whatToDo')}
{!isRecoveryMode && onSwitchToLocal && ( )}
)}
); }