"use client"; import { invoke } from "@tauri-apps/api/core"; import { emit } from "@tauri-apps/api/event"; import { useCallback, useEffect, useState } from "react"; import { LuShield, LuUpload } from "react-icons/lu"; import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RippleButton } from "@/components/ui/ripple"; import { ScrollArea } from "@/components/ui/scroll-area"; import { getCurrentOS } from "@/lib/browser-utils"; import type { VpnImportResult, VpnType } from "@/types"; interface VpnImportDialogProps { isOpen: boolean; onClose: () => void; } type ImportStep = "dropzone" | "vpn-preview" | "vpn-result"; interface VpnPreviewData { content: string; filename: string; detectedType: VpnType | null; endpoint: string | null; } const detectVpnType = ( content: string, filename: string, ): { isVpn: boolean; type: VpnType | null; endpoint: string | null } => { const lowerFilename = filename.toLowerCase(); if ( lowerFilename.endsWith(".conf") && content.includes("[Interface]") && content.includes("[Peer]") ) { const endpointMatch = content.match(/Endpoint\s*=\s*([^\s\n]+)/i); return { isVpn: true, type: "WireGuard", endpoint: endpointMatch ? endpointMatch[1] : null, }; } if ( lowerFilename.endsWith(".ovpn") || (content.includes("remote ") && (content.includes("client") || content.includes("dev tun"))) ) { const remoteMatch = content.match(/remote\s+(\S+)(?:\s+(\d+))?/i); const endpoint = remoteMatch ? `${remoteMatch[1]}${remoteMatch[2] ? `:${remoteMatch[2]}` : ""}` : null; return { isVpn: true, type: "OpenVPN", endpoint }; } return { isVpn: false, type: null, endpoint: null }; }; export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) { const [step, setStep] = useState("dropzone"); const [isDragOver, setIsDragOver] = useState(false); const [vpnPreview, setVpnPreview] = useState(null); const [vpnName, setVpnName] = useState(""); const [vpnImportResult, setVpnImportResult] = useState(null); const [isImporting, setIsImporting] = useState(false); const os = getCurrentOS(); const modKey = os === "macos" ? "⌘" : "Ctrl"; const resetState = useCallback(() => { setStep("dropzone"); setIsDragOver(false); setVpnPreview(null); setVpnName(""); setVpnImportResult(null); setIsImporting(false); }, []); const handleClose = useCallback(() => { resetState(); onClose(); }, [resetState, onClose]); const processContent = useCallback((content: string, filename: string) => { const detection = detectVpnType(content, filename); if (!detection.isVpn) { toast.error("Content does not appear to be a valid VPN configuration"); return; } setVpnPreview({ content, filename, detectedType: detection.type, endpoint: detection.endpoint, }); const baseName = filename .replace(/\.(conf|ovpn)$/i, "") .replace(/_/g, " ") .replace(/-/g, " "); setVpnName(baseName || `${detection.type} VPN`); setStep("vpn-preview"); }, []); const handleFileRead = useCallback( (file: File) => { const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; processContent(content, file.name); }; reader.onerror = () => { toast.error("Failed to read file"); }; reader.readAsText(file); }, [processContent], ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const files = Array.from(e.dataTransfer.files); const validFile = files.find( (f) => f.name.endsWith(".conf") || f.name.endsWith(".ovpn"), ); if (validFile) { handleFileRead(validFile); } else { toast.error("Please drop a .conf or .ovpn file"); } }, [handleFileRead], ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); useEffect(() => { if (!isOpen || step !== "dropzone") return; const handlePaste = (e: ClipboardEvent) => { const text = e.clipboardData?.getData("text"); if (text) { processContent(text, "pasted.conf"); } }; document.addEventListener("paste", handlePaste); return () => { document.removeEventListener("paste", handlePaste); }; }, [isOpen, step, processContent]); const handleImport = useCallback(async () => { if (!vpnPreview) return; setIsImporting(true); try { const result = await invoke("import_vpn_config", { content: vpnPreview.content, filename: vpnPreview.filename, name: vpnName.trim() || null, }); setVpnImportResult(result); setStep("vpn-result"); if (result.success) { await emit("vpn-configs-changed"); } } catch (error) { toast.error( error instanceof Error ? error.message : "Failed to import VPN config", ); } finally { setIsImporting(false); } }, [vpnPreview, vpnName]); return ( Import VPN Config {step === "dropzone" && "Import a WireGuard (.conf) or OpenVPN (.ovpn) configuration file"} {step === "vpn-preview" && "Review the VPN configuration to import"} {step === "vpn-result" && "VPN import completed"} {step === "dropzone" && (
document.getElementById("vpn-file-input")?.click()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); document.getElementById("vpn-file-input")?.click(); } }} >

Drop a VPN config file here or click to browse
(.conf for WireGuard, .ovpn for OpenVPN)

{ const file = e.target.files?.[0]; if (file) handleFileRead(file); e.target.value = ""; }} />

Paste from clipboard with {modKey}+V

)} {step === "vpn-preview" && vpnPreview && (
{vpnPreview.detectedType} Configuration
{vpnPreview.endpoint && (
Endpoint: {vpnPreview.endpoint}
)}
setVpnName(e.target.value)} />
                  {vpnPreview.content.slice(0, 1000)}
                  {vpnPreview.content.length > 1000 && "..."}
                
)} {step === "vpn-result" && vpnImportResult && (
{vpnImportResult.success ? (
VPN Imported Successfully
{vpnImportResult.name} ({vpnImportResult.vpn_type})
) : (
Import Failed
{vpnImportResult.error}
)}
)} {step === "dropzone" && ( Cancel )} {step === "vpn-preview" && ( <> Back void handleImport()} > Import VPN )} {step === "vpn-result" && ( Done )}
); }