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
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
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
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.
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.