import { app, BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions, nativeImage, shell, Tray } from 'electron'; import path from 'node:path'; import started from 'electron-squirrel-startup'; import { APP_NAME, DEFAULT_STATUS, GATEWAY_HOST, MANAGEMENT_URL, POLL_INTERVAL_MS, WINDOW_HEIGHT, WINDOW_WIDTH } from './config'; import { NetBirdClient } from './netbird'; import { baseStatus, normalizeStatus } from './status'; import { NetworkStatus, UIStatus } from './types'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { app.quit(); } const singleInstanceLock = app.requestSingleInstanceLock() if (!singleInstanceLock) app.quit(); else { app.on('second-instance', () => { if (window) { if (window.isMinimized()) window.restore(); window.focus(); } }); app.on('before-quit', disconnect); } app.whenReady().then(() => { createWindow(); createTray(); refreshStatus(); pollTimer = setInterval(refreshStatus, POLL_INTERVAL_MS); }); app.on('before-quit', () => { if (pollTimer) clearInterval(pollTimer); }); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here. let tray: Tray; let window: BrowserWindow; let pollTimer: NodeJS.Timeout; let autoConnect = false; let cachedStatus = DEFAULT_STATUS; function resourcePath(name: string) { if (app.isPackaged) { return path.join(process.resourcesPath, 'resources', name); } return path.join(app.getAppPath(), 'resources', name); } function netbirdClient() { return new NetBirdClient(resourcePath(process.platform === "win32" ? 'netbird.exe' : 'netbird')); } function withAutoConnect(status: NetworkStatus): UIStatus { return { ...status, autoConnect }; } function broadcastStatus() { if (window && !window.isDestroyed()) { window.webContents.send('status', withAutoConnect(cachedStatus)); } } function updateTray() { if (!tray) return; const label = cachedStatus.state === 'connected' ? `${APP_NAME}: Connected` : APP_NAME; tray.setToolTip(label); const template: MenuItemConstructorOptions[] = []; const connected = cachedStatus.state === "connected"; const busy = ['connecting', 'disconnecting'].includes(cachedStatus.state); template.push({ label: connected ? 'Disconnect' : 'Connect', click: connected ? disconnect : connect, enabled: !busy }); template.push( { label: 'Logout', click: logout }, { role: 'quit' } ); tray.setContextMenu(Menu.buildFromTemplate(template)); } function setStatus(status: NetworkStatus) { cachedStatus = { ...cachedStatus, ...status, gatewayHost: GATEWAY_HOST }; broadcastStatus(); updateTray(); return cachedStatus; } async function refreshStatus() { const result = await netbirdClient().status(); if (!result.ok) { return setStatus(baseStatus({ state: 'disconnected', title: 'Disconnected', message: 'Click the flame to install the Vynte network helper.' })); } const status = normalizeStatus(result.stdout, cachedStatus); if (status.state === "disconnected" && cachedStatus.state === "connecting") return; setStatus(status); } async function connect() { setStatus({ state: 'connecting', title: 'Connecting...', message: 'Starting secure Vynte access...' }); const client = netbirdClient(); const serviceReady = await client.ensureService(); if (!serviceReady) { return setStatus({ state: 'error', title: 'Connection failed', message: `${GATEWAY_HOST} helper did not become ready.` }); } const result = await client.connect(); if (!result.ok) { const output = `${result.stdout}\n${result.stderr}`.trim(); return setStatus({ state: 'error', title: 'Connection failed', message: output || `${GATEWAY_HOST} command failed.` }); } return refreshStatus(); } async function disconnect() { setStatus({ state: 'disconnecting', title: 'Disconnecting...', message: 'Closing secure Vynte access...' }); await netbirdClient().disconnect(); return refreshStatus(); } async function logout() { setStatus({ state: 'loggingOut', title: 'Logging out', message: "Removing this PC's NetBird registration." }); await netbirdClient().logout(); return refreshStatus(); } function createWindow() { window = new BrowserWindow({ width: WINDOW_WIDTH, height: WINDOW_HEIGHT, show: false, resizable: false, frame: false, alwaysOnTop: true, skipTaskbar: true, backgroundColor: '#191b20', icon: resourcePath("VynteIcon.png"), webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true } }); // and load the index.html of the app. if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { window.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); } else { window.loadFile( path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`), ); } window.on('blur', () => window.hide()); } function showWindow() { const bounds = tray.getBounds(); const x = Math.round(bounds.x - WINDOW_WIDTH); const y = Math.round(bounds.y - WINDOW_HEIGHT); window.setBounds({ width: WINDOW_WIDTH, height: WINDOW_HEIGHT, x, y }); window.show(); window.focus(); broadcastStatus(); } function createTray() { const icon = nativeImage.createFromPath(resourcePath('VynteIcon.png')).resize({ width: 18, height: 18 }); tray = new Tray(icon); updateTray(); tray.on('click', showWindow); } ipcMain.handle('status:get', () => withAutoConnect(cachedStatus)); ipcMain.handle('status:refresh', refreshStatus); ipcMain.handle('vpn:connect', connect); ipcMain.handle('vpn:disconnect', disconnect); ipcMain.handle('vpn:logout', logout); ipcMain.handle('auto:toggle', () => { autoConnect = !autoConnect; broadcastStatus(); return autoConnect; }); ipcMain.handle('app:quit', () => app.quit()); ipcMain.handle('external:openGateway', () => shell.openExternal(MANAGEMENT_URL));