VIDSOL-649: [WEB] Add Electron Desktop Application Support#420
VIDSOL-649: [WEB] Add Electron Desktop Application Support#420
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an Electron desktop application (Nx project + build/dev/package tooling) around the existing React frontend by serving frontend/dist from a localhost HTTP server inside the Electron main process, with custom screen-share picker UI, permissions handling, clipboard IPC, and accompanying docs/assets.
Changes:
- Introduces a new
electron/application (main + preload scripts, picker/about windows, packaging config, macOS entitlements/resources). - Adds Electron dev/package scripts (including macOS Info.plist patching) and updates workspace scripts/README.
- Updates a small set of frontend utilities/components to support Electron clipboard + share URL origin via
TUNNEL_DOMAIN.
Reviewed changes
Copilot reviewed 26 out of 47 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/generateElectronIcons.ts | Generates Electron app/tray icons (currently macOS-tooling dependent). |
| scripts/electronPackage.ts | Build + package flow for electron-builder. |
| scripts/electronDev.ts | Dev build + macOS Info.plist patch + launch flow. |
| scripts/dev.ts | Adds yarn dev electron target (backend + Electron). |
| package.json | Adds Electron scripts and Electron-related devDependencies. |
| frontend/src/utils/clipboard.ts | Clipboard utility with Electron IPC fallback. |
| frontend/src/types/electron.d.ts | Renderer ambient typings for window.electron. |
| frontend/src/hooks/useRoomShareUrl.tsx | Uses TUNNEL_DOMAIN for externally shareable URLs (Electron-friendly). |
| frontend/src/components/MeetingRoom/SmallViewportHeader/SmallViewportHeader.tsx | Uses clipboard utility for copy-link. |
| frontend/src/components/MeetingRoom/ParticipantList/ParticipantList.tsx | Uses clipboard utility for copy-link. |
| electron/tsconfig.json | TypeScript config for Electron main/preload compilation. |
| electron/project.json | Nx project targets for Electron (build/dev/package/ts-check). |
| electron/preload.ts | Exposes minimal window.electron API via contextBridge. |
| electron/preload.d.ts | Electron preload ambient typings (currently incomplete vs exposed API). |
| electron/picker-preload.ts | Context bridge for the screen-share picker window. |
| electron/picker.html | Custom screen-share source picker UI. |
| electron/main.ts | Electron main process: localhost server, permissions, CSP, picker IPC, tray, updates, windows. |
| electron/entitlements.mac.plist | macOS hardened runtime entitlements for media/screen/network. |
| electron/electron-builder.json | electron-builder packaging configuration. |
| electron/build-resources/en.lproj/InfoPlist.strings | Localized macOS permission strings (en). |
| electron/build-resources/de.lproj/InfoPlist.strings | Localized macOS permission strings (de). |
| electron/build-resources/es.lproj/InfoPlist.strings | Localized macOS permission strings (es). |
| electron/build-resources/es_MX.lproj/InfoPlist.strings | Localized macOS permission strings (es_MX). |
| electron/build-resources/it.lproj/InfoPlist.strings | Localized macOS permission strings (it). |
| electron/assets/tray-template.png | macOS tray template icon asset. |
| electron/assets/tray-template@2x.png | macOS tray template icon asset (@2x). |
| electron/assets/tray-icon.png | Windows/Linux tray icon asset. |
| electron/assets/tray-icon@2x.png | Windows/Linux tray icon asset (@2x). |
| electron/assets/app-icon.png | App/window icon asset. |
| electron/assets/AppIcon.iconset/icon_16x16.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_16x16@2x.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_32x32.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_32x32@2x.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_64x64.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_64x64@2x.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_128x128.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_128x128@2x.png | macOS iconset raster asset. |
| electron/assets/AppIcon.iconset/icon_256x256.png | macOS iconset raster asset. |
| electron/about.html | Vonage-branded About dialog window. |
| electron/ELECTRON.md | Architecture and developer documentation for Electron support. |
| README.md | Adds Electron Desktop App docs/quick start section. |
| .prettierignore | Ignores .claude/. |
📸 ScreenshotsPlease reply to this comment with the screenshots by dragging and dropping them here. Label each one:
Once uploaded, the image URLs will be added to the PR description. |
- Electron main process embedding Express backend via localhost - Custom screen sharing picker with tabbed Entire Screen / Window UI - macOS TCC permission handling (camera, mic, screen recording) - Vonage-branded About dialog, tray menu, and app icons - Clipboard support routed through Electron's main-process API - Content Security Policy configured for Vonage/OpenTok domains - Dev mode script (yarn dev:electron) and packaging script (yarn build:electron) - electron-builder config for macOS (dmg), Windows (nsis), Linux (AppImage) - ELECTRON.md documentation covering architecture and setup - Updated README.md with Electron section - Localized InfoPlist.strings for en, de, es, es_MX, it Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix path traversal vulnerability in static file server (resolve + prefix check) - Scope permission handlers to trusted localhost origin only - Use BrowserWindow.fromWebContents() instead of title-based picker window lookup - Guard against concurrent screen picker requests (reuse existing window) - Enable sandbox: true on all BrowserWindows for defense-in-depth - Add platform guard to icon generation script (skip on non-macOS) - Fix ELECTRON.md documentation inconsistency about backend embedding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded CSS colors with VERA design system tokens for consistency with the main app. About dialog uses light theme tokens, Picker uses dark theme tokens. Both now load Inter font from Google Fonts CDN. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Generate app-icon.ico with 6 resolutions (16–256px) for sharp Windows taskbar/Start menu icons - Add arm64 architecture targets for Windows (NSIS) and Linux (AppImage) - Point Windows icon config to .ico instead of .png - Document .ico as a committed asset in generateElectronIcons.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Fix path traversal guard: use path.relative() instead of startsWith() to prevent prefix collision bypasses - Wrap decodeURIComponent in try/catch, return 400 on malformed URLs - Only SPA-fallback to index.html for extensionless routes; return 404 for missing static assets so build issues surface immediately - Fix plist idempotence check: match CFBundleDisplayName specifically instead of any occurrence of the product name string - Add Escape key handler to picker for keyboard cancel - Add periodic thumbnail refresh (2s interval) via new IPC channel - Fix ELECTRON.md: clarify backend is not bundled in packaged builds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… management - About dialog: add "Check for Updates" button (uses electron-updater in packaged builds, shows friendly message in dev mode) - Picker window: increase size by 20% (864×624) for better visibility - Picker: set alwaysOnTop with 'floating' level so it cannot lose focus - About: also floats above picker when both are open - Dismiss picker automatically if About is opened (context switch) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ELECTRON_MAJOR version detection at startup for runtime API branching
- setDisplayMediaRequestHandler: pass { useSystemPicker: false } so our
custom picker is used on Electron 39+ (which defaults to system picker)
- Permission handlers: version-branched for Electron ≤38 vs 39+ patterns
- Screen recording pre-check: always attempt getSources() instead of
bailing on getMediaAccessStatus('denied') — macOS TCC can report stale
status after binary swaps
- Dynamic import electron-updater everywhere (no top-level import)
- Add NSAudioCaptureUsageDescription to plist for macOS 14.2+ / Electron 39+
- setDisplayMediaRequestHandler: runtime feature check with graceful fallback
Tested on Electron 36.9.5 and 39.8.3 — screen share, permissions, picker,
About dialog, and Check for Updates all working on both versions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tested versions (36, 39, 41), version-specific adaptations, and step-by-step instructions for upgrading Electron and resetting macOS TCC permissions after binary swaps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the desktopCapturer.getSources() startup probe to fire after win.loadURL() completes, so the macOS permission dialog appears in front of the app window rather than behind it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0309606 to
8bb92c7
Compare
| "electron": "36.9.5", | ||
| "electron-builder": "^25.1.8", | ||
| "electron-updater": "^6.8.3", |
There was a problem hiding this comment.
electron-updater is listed under devDependencies, but it’s dynamically imported at runtime in the packaged app (see electron/main.ts). electron-builder typically bundles only production dependencies, so packaged builds may not include electron-updater, causing update checks to always fall back (silent no-op). Move electron-updater to dependencies (or explicitly include it in the packaged app) so auto-update works in release artifacts.
electron/main.ts
Outdated
| const ALLOWED_REQUEST = [ | ||
| 'media', | ||
| 'display-capture', | ||
| 'mediaKeySystem', | ||
| 'notifications', | ||
| 'clipboard-write', | ||
| 'clipboard-read', | ||
| ]; |
There was a problem hiding this comment.
configureMediaPermissions() allows clipboard-read (and notifications) for the renderer. The frontend changes in this PR only require clipboard write (and Electron already routes writes through IPC), and there’s no usage of clipboard-read in the web code. Consider removing clipboard-read (and any other unused permissions) from the allowed lists to reduce the impact of any renderer XSS to data-exfiltration.
| * Creates a shareable link to the waiting room for the current meeting room. | ||
| * When running inside Electron, window.location.origin is 'http://localhost:PORT' | ||
| * which is not externally reachable. If TUNNEL_DOMAIN is configured (e.g. an ngrok | ||
| * domain), it is used as the base so the link can be shared with web users. | ||
| * @returns {string} - The shareable link. | ||
| */ | ||
| const useRoomShareUrl = (): string => { | ||
| const roomName = useRoomName(); | ||
| const { origin } = window.location; | ||
| const origin = env.TUNNEL_DOMAIN ? `https://${env.TUNNEL_DOMAIN}` : window.location.origin; |
There was a problem hiding this comment.
The docstring says this origin override is specifically for Electron, but the implementation uses env.TUNNEL_DOMAIN unconditionally (it will also affect normal web builds whenever the env var is set). Either update the comment to reflect the real behavior, or gate the override behind an Electron check (e.g., window.electron?.isElectron) so the semantics match the documentation.
| * The backend runs in the background; the Electron app is the foreground process. | ||
| */ | ||
| function devElectron(): void { | ||
| runCommand("concurrently --kill-others 'nx run backend:dev' 'npx tsx scripts/electronDev.ts'"); |
There was a problem hiding this comment.
The concurrently command is wrapped in single quotes. On Windows shells (cmd/powershell), single quotes are not treated as string quotes in the same way, which can break yarn dev electron—especially important since this PR aims for cross-platform Electron support. Prefer double quotes for the subcommands (or use concurrently --raw / --command patterns) so it runs consistently across platforms.
electron/main.ts
Outdated
| let APP_VERSION = 'unknown'; | ||
| let SDK_VERSION = 'unknown'; | ||
|
|
||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| APP_VERSION = (require('../../package.json') as { version: string }).version; | ||
| } catch { | ||
| /* leave as "unknown" */ | ||
| } | ||
|
|
||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| SDK_VERSION = (require('@vonage/client-sdk-video/package.json') as { version: string }).version; | ||
| } catch { | ||
| /* leave as "unknown" */ | ||
| } | ||
|
|
||
| // ─── Electron version detection ────────────────────────────────────────────── | ||
| // Parse the major version once at startup so we can adapt to API changes | ||
| // across Electron 36–41+ without hard-coding version checks everywhere. | ||
| const ELECTRON_MAJOR = parseInt(process.versions.electron?.split('.')[0] ?? '0', 10); | ||
| console.log(`[Electron] Running Electron ${process.versions.electron} (major: ${ELECTRON_MAJOR})`); | ||
|
|
||
| // ─── Paths ──────────────────────────────────────────────────────────────────── | ||
|
|
||
| const IS_PACKAGED = app.isPackaged; | ||
|
|
||
| const ASSETS_DIR = IS_PACKAGED | ||
| ? path.join(process.resourcesPath, 'electron-assets') | ||
| : path.join(__dirname, '../assets'); |
There was a problem hiding this comment.
Several newly introduced identifiers use acronyms/initialisms (e.g., SDK_VERSION, APP_VERSION, ELECTRON_MAJOR, IS_PACKAGED, ASSETS_DIR). Repository guidelines ban acronyms in names; prefer fully descriptive names (e.g., videoSdkVersion, applicationVersion, electronMajorVersion, isPackaged, assetsDirectory) to keep naming consistent and readable.
There was a problem hiding this comment.
Fixed the naming convention issue flagged in this comment. Renamed all constants to follow the repo's no-acronyms rule:
APP_VERSION→applicationVersionSDK_VERSION→videoSdkVersionELECTRON_MAJOR→electronMajorVersionIS_PACKAGED→isPackagedASSETS_DIR→assetsDirectorySTATIC_PORT→staticPortMIME→mimeTypesALLOWED_REQUEST→allowedRequestPermissionsALLOWED_CHECK→allowedCheckPermissionsconfigureCSP()→configureContentSecurityPolicy()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace SCREAMING_CASE acronym identifiers with descriptive camelCase names per repository guidelines: APP_VERSION → applicationVersion, SDK_VERSION → videoSdkVersion, ELECTRON_MAJOR → electronMajorVersion, IS_PACKAGED → isPackaged, ASSETS_DIR → assetsDirectory, STATIC_PORT → staticPort, MIME → mimeTypes, configureCSP → configureContentSecurityPolicy, ALLOWED_REQUEST/CHECK → allowedRequestPermissions/allowedCheckPermissions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ments Security (per Electron security checklist): - Add will-navigate handler to block navigation outside trusted localhost - Add setWindowOpenHandler to deny new window creation - Add render-process-gone handler with crash recovery dialog - Add unresponsive handler with wait/restart dialog - Disable spellchecker to prevent data sent to OS spell services Developer experience: - Add IPC channel reference documentation at top of main.ts - Add version detection comments explaining why branching is needed - Add descriptive error logging for static server failures - Add CSP confirmation log on startup - Add Escape/Cmd+W keyboard shortcuts to close About dialog - Remove auto-opening DevTools (still accessible via View menu) References: - https://www.electronjs.org/docs/latest/tutorial/security - Items #13 (limit navigation) and #14 (limit new windows) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Latest commits: Security hardening + Developer experienceSecurity best practices (per Electron Security Checklist)
Developer experience improvements
Naming convention fixRenamed all SCREAMING_CASE constants to camelCase per repo guidelines (no acronyms rule): |
New helper script to test the app with any Electron version or channel
(stable, beta, alpha, nightly). Automatically installs the requested
version, resets macOS TCC permissions, runs TypeScript check, launches
the app, and restores the original version on exit.
Usage: yarn test:electron-version 41.0.3
yarn test:electron-version beta
Also expanded ELECTRON.md with detailed version testing docs including
release channel table and manual swap instructions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New script (yarn test:tcc) that checks macOS TCC permission status for Camera, Microphone, and Screen Recording. Shows Info.plist patching status and suggests fixes for common permission issues. Added troubleshooting table to ELECTRON.md covering all TCC issues we encountered during development: binary hash changes, permission dialog ordering, responsible process attribution, and version swap resets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d8f3648 to
791f3cc
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
The previous commit excluded only 3 specific .ts files, but SonarCloud also analyzes .html, .json, and other files in the electron/ directory. Using electron/** catches all Electron main-process files that cannot be covered by the existing Vitest browser test suite. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>











What is this PR doing?
Adds full Electron desktop application support to the Vonage Video React Reference App, allowing the same codebase to run as both a web app and a native desktop application on macOS, Windows, and Linux.
require()and serves the React frontend over localhost, soBrowserRouter,localStorage,window.location, and all existing web logic works unchangeddesktopCapturerintegration. Supports both the Vonage SDK'sdesktop-capturer-get-sourcesIPC path and Chromium'ssetDisplayMediaRequestHandlerInfo.plistpatching (NSCameraUsageDescription,NSMicrophoneUsageDescription),setPermissionCheckHandler/setPermissionRequestHandler, andsystemPreferences.getMediaAccessStatus()checks at startupclipboardmodule via IPC, replacingnavigator.clipboardwhich requires secure context focus that Electron doesn't always provide.icnsicon set, Windows.ico(16–256px, 6 resolutions), and cross-platform PNG tray icons, all generated from the Vonage logoyarn dev:electron) — builds frontend + backend + Electron TypeScript, patchesInfo.plistfor dev, launches viaopen -non macOS for correct TCC attributionyarn build:electron) — electron-builder config for macOS (DMG, arm64+x64), Windows (NSIS, x64+arm64), Linux (AppImage, x64+arm64) with hardened runtime and entitlementselectron-updaterwill-navigatehandler to block navigation outside localhost,setWindowOpenHandlerto deny new windows,render-process-gonecrash recovery,unresponsivehandler, spellchecker disabled (per Electron Security Checklist)alwaysOnTop: 'floating'level; About dialog floats above the pickerdesktopCapturer.getSources()probe fires afterwin.loadURL()completes so the macOS permission dialog appears in front of the appyarn test:electron-version <version>to test with any Electron version/channel (auto-restores original),yarn test:tccto diagnose macOS TCC permissionsArchitecture
The Electron app uses a localhost embedding strategy: the main process starts the Express backend server, then loads
http://localhost:<port>in aBrowserWindow. This means:useRoomShareUrl.tsx— detects Electron context to show a "copy link" button instead of relying onwindow.location.origin(which would belocalhostin Electron)clipboard.ts— utility that routes clipboard writes through Electron's IPC when availableParticipantList.tsx/SmallViewportHeader.tsx— use the clipboard utility for share URL copyingelectron.d.ts— TypeScript declarations forwindow.electronCross-Platform Support
The codebase is designed to work on macOS, Windows, and Linux from a single codebase with platform-specific handling where needed:
.icns(16–512@2x).ico(16–256px).png(512px)Note: This PR has been developed and tested on macOS. Windows and Linux builds are architecturally supported (all platform-specific code uses
process.platformguards), but have not yet been tested on those platforms. Community testing on Windows and Linux is welcome.Electron Version Compatibility
The code is version-adaptive — it detects the Electron major version at startup and adapts API calls accordingly. Tested and verified on:
Key version adaptations:
setDisplayMediaRequestHandlerrequires{ useSystemPicker: false }to use the custom picker instead of the OS-level pickerNSAudioCaptureUsageDescriptionadded to Info.plist for CoreAudio Tap APIelectron-updaterdynamically imported (no hard dependency at startup)New Files
electron/main.tselectron/preload.tswindow.electronAPI (clipboard, screen capture, external links)electron/preload.d.tselectron/picker-preload.tselectron/picker.htmlelectron/about.htmlelectron/project.jsonbuild,dev,package,ts-checktargetselectron/tsconfig.jsonelectron/electron-builder.jsonelectron/entitlements.mac.plistelectron/ELECTRON.mdelectron/assets/.icns,.ico, iconset PNGs, tray icons)electron/build-resources/InfoPlist.stringsfor permission descriptionsscripts/electronDev.tsopen -nlaunchscripts/electronPackage.tsscripts/generateElectronIcons.tsfrontend/src/utils/clipboard.tsfrontend/src/types/electron.d.tswindow.electrontype declarationsModified Files
package.jsonelectron,electron-builderdevDependencies;dev:electron,build:electronscriptsscripts/dev.tsdevElectron()function andelectrontarget in main switchREADME.mdfrontend/src/hooks/useRoomShareUrl.tsxfrontend/src/components/MeetingRoom/ParticipantList/ParticipantList.tsxfrontend/src/components/MeetingRoom/SmallViewportHeader/SmallViewportHeader.tsx.prettierignore.claude/to ignore listfrontend/tsconfig.jsonmacOS TCC (Transparency, Consent, Control) Permissions
Camera, microphone, and screen recording permissions on macOS require special handling in Electron:
NSCameraUsageDescriptionandNSMicrophoneUsageDescriptionare required for macOS to show permission prompts. The dev script patches the Electron framework'sInfo.plistat startupopen -nlaunch — In dev mode, the app is launched viaopen -n Electron.app --args ...instead of spawning as a child process. This makes Electron the "responsible process" for TCC, so macOS correctly attributes permission requests to the Electron app rather than the parent terminal/IDEentitlements.mac.plistdeclares camera, microphone, screen capture, JIT, unsigned executable memory, and network entitlements for hardened runtime buildsInfoPlist.stringsfiles provide localized descriptions for permission prompts in en, de, es, es_MX, and itScreen Sharing
The screen sharing implementation supports two code paths:
window.electron.desktopCapturer.getSources()via IPC, which opens the custom picker and returns the selected sourcesetDisplayMediaRequestHandlerinterceptsgetDisplayMedia()calls and routes them through the same pickerThe picker UI features:
How should this be manually tested?
Prerequisites:
yarn installbackend/.envhas valid Vonage credentials (API key, App ID, private key)Quick Start:
A. App Launch and Landing Page
yarn dev:electronB. Camera and Microphone
C. Screen Sharing
D. Clipboard and Share URL
E. Meeting Functionality
F. Web App Regression
yarn dev(regular web mode)navigator.clipboard(not Electron IPC)getDisplayMedia(not Electron picker)G. Cross-Platform (Windows / Linux)
yarn dev:electronon Windowsyarn build:electrondist/electron/yarn dev:electronon Linuxyarn build:electrondist/electron/Screenshots
Waiting Room — Permission Prompts
Waiting Room — Camera Working
About Dialog
Tray Icon
Screen Sharing Picker
What are the relevant tickets?
VIDSOL-649 — [WEB] Add Electron Desktop Application Support
Documentation
electron/ELECTRON.md— comprehensive architecture and developer documentation added to the repoREADME.md— updated with Electron quick-start sectionChecklist
develop(notmain)Known Issuedocs/KNOWN_ISSUES.md?IssuesGenerated with Claude Code