Skip to Content
ElectronRenderer and IPC

Renderer and IPC

The SDK only runs in the main process. Your windows, which show the activation screen and the locked screens, reach it through IPC: the main process handles requests, and sends each state change to the windows. A preload script exposes a small, safe API to the renderer.

Handle requests in the main process

main.ts
import { BrowserWindow, ipcMain, shell } from "electron"; import { LicenseError, LicenseManager } from "@licenseman/electron"; const license = new LicenseManager({ productId: PRODUCT_ID, publicKey: PUBLIC_KEY }); ipcMain.handle("license:get", () => ({ state: license.getState(), offline: license.getOfflineState(), latestAllowedVersion: license.getLatestAllowedVersion(), })); ipcMain.handle("license:activate", async (_event, key: string) => { try { return { ok: true, state: await license.setLicenseKey(key) }; } catch (error) { if (error instanceof LicenseError) { return { ok: false, code: error.code, latestAllowedVersion: error.latestAllowedVersion, }; } return { ok: false, code: "unknown" }; } }); ipcMain.handle("license:refresh", () => license.refresh()); ipcMain.handle("license:open-portal", () => shell.openExternal(license.getPortalUrl())); license.onStateChange((state) => { for (const window of BrowserWindow.getAllWindows()) { window.webContents.send("license:state", state); } });

Expose them in the preload script

preload.ts
import { contextBridge, ipcRenderer } from "electron"; contextBridge.exposeInMainWorld("license", { get: () => ipcRenderer.invoke("license:get"), activate: (key: string) => ipcRenderer.invoke("license:activate", key), refresh: () => ipcRenderer.invoke("license:refresh"), openPortal: () => ipcRenderer.invoke("license:open-portal"), onStateChange: (listener: (state: string) => void) => { const handler = (_event: unknown, state: string) => listener(state); ipcRenderer.on("license:state", handler); return () => ipcRenderer.removeListener("license:state", handler); }, });

Use them in the window

renderer.ts
const { state } = await window.license.get(); render(state); window.license.onStateChange(render); form.addEventListener("submit", async (event) => { event.preventDefault(); const result = await window.license.activate(input.value); if (!result.ok) showError(messages[result.code]); });

Return errors from ipcMain.handle, don’t throw them. Electron only passes an error’s message to the renderer, wrapped in Error invoking remote method 'license:activate': …: its code and latestAllowedVersion are lost. Return a plain object with the code instead, like above.

Gating your windows

Your main process decides which window to show from the state. A simple pattern: one window, which shows your app when the state is ACTIVE, and the activation or locked screen otherwise.

main.ts
app.whenReady().then(async () => { license = new LicenseManager({ productId: PRODUCT_ID, publicKey: PUBLIC_KEY }); const window = new BrowserWindow({ webPreferences: { preload: join(__dirname, "preload.js") }, }); const state = await license.ready(); window.loadFile(state === "ACTIVE" ? "app.html" : "activate.html"); license.onStateChange((state) => { const page = state === "ACTIVE" ? "app.html" : "activate.html"; if (!window.webContents.getURL().endsWith(page)) window.loadFile(page); }); });

The check that matters happens in the main process: a customer can open the developer tools of a window, but not change the state the main process sees. Keep features that must stay locked, like exporting or saving, behind a check of license.getState() in the main process, not only in your UI.

Sending only what’s needed

Only expose what your windows need: the state, the activation, refresh() and the customer portal. Don’t expose the LicenseManager itself, the stored key, or a generic way to call its methods: anything in the preload script can be called by any code running in the window.

To show the key in your About window, like the last 5 characters, send only those from the main process, with license.getLicenseKey()?.slice(-5).

Edge cases

A window opened after a state change didn’t receive it: always start with license:get, then listen.

Several windows each receive every state change, from BrowserWindow.getAllWindows().

Reloading the window, like with Cmd+R in development, doesn’t affect the SDK: the state lives in the main process.

sandbox: true preload scripts can’t import npm packages, but only contextBridge and ipcRenderer are needed, which are available.