2026-05-21 09:50:51 +07:00
|
|
|
/**
|
|
|
|
|
* Velxio Desktop SPA hooks — mounted from main.tsx when VITE_DESKTOP is set.
|
|
|
|
|
*
|
|
|
|
|
* Two responsibilities in Phase 3:
|
|
|
|
|
*
|
|
|
|
|
* 1. Show the welcome / sign-in screen if the stored license key
|
|
|
|
|
* fails validation. While the welcome screen is up, the SPA's
|
|
|
|
|
* editor is hidden behind it so the user can't compile / run.
|
|
|
|
|
* 2. Mount desktop-only side panels (the ESP32 QEMU prompt).
|
|
|
|
|
*
|
|
|
|
|
* Phase 4 will add the offline JWT cache and grace banners on top.
|
|
|
|
|
*
|
|
|
|
|
* Pure OSS still runs without any of this (the dynamic import is
|
|
|
|
|
* tree-shaken when the env flag is unset). The pro overlay also
|
|
|
|
|
* doesn't load in desktop builds, so this module owns the desktop UI.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
|
|
|
import { createElement } from 'react';
|
|
|
|
|
import { createElement as h, Fragment } from 'react';
|
|
|
|
|
import { DesktopWelcomePage } from './DesktopWelcomePage';
|
|
|
|
|
import { Esp32QemuPrompt } from './Esp32QemuPrompt';
|
|
|
|
|
import { GraceBanner } from './GraceBanner';
|
|
|
|
|
import { invoke, isTauri, type ValidationResult } from './tauriBridge';
|
2026-05-23 04:08:46 +07:00
|
|
|
import { installDesktopMenuListener } from './menu';
|
|
|
|
|
import { dlog } from './log';
|
2026-05-21 09:50:51 +07:00
|
|
|
import './desktop.css';
|
|
|
|
|
|
|
|
|
|
let mounted = false;
|
|
|
|
|
let welcomeRoot: Root | null = null;
|
|
|
|
|
let welcomeHost: HTMLElement | null = null;
|
|
|
|
|
let sidePanelRoot: Root | null = null;
|
|
|
|
|
|
|
|
|
|
function unmountWelcome(): void {
|
|
|
|
|
if (welcomeRoot) {
|
|
|
|
|
try { welcomeRoot.unmount(); } catch { /* noop */ }
|
|
|
|
|
welcomeRoot = null;
|
|
|
|
|
}
|
|
|
|
|
if (welcomeHost) {
|
|
|
|
|
welcomeHost.remove();
|
|
|
|
|
welcomeHost = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mountWelcome(): void {
|
|
|
|
|
if (welcomeRoot) return;
|
|
|
|
|
welcomeHost = document.createElement('div');
|
|
|
|
|
welcomeHost.id = 'velxio-desktop-welcome-root';
|
|
|
|
|
document.body.appendChild(welcomeHost);
|
|
|
|
|
welcomeRoot = createRoot(welcomeHost);
|
|
|
|
|
welcomeRoot.render(
|
|
|
|
|
createElement(DesktopWelcomePage, {
|
|
|
|
|
onAuthorised: () => unmountWelcome(),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mountSidePanels(): void {
|
|
|
|
|
if (sidePanelRoot) return;
|
|
|
|
|
const host = document.createElement('div');
|
|
|
|
|
host.id = 'velxio-desktop-side-panels';
|
|
|
|
|
document.body.appendChild(host);
|
|
|
|
|
sidePanelRoot = createRoot(host);
|
|
|
|
|
// Single root for both side-panel surfaces so we don't burn extra
|
|
|
|
|
// React roots on the document. Both renderers return null when
|
|
|
|
|
// they have nothing to show, so the only cost is the subscription
|
|
|
|
|
// they each install.
|
|
|
|
|
sidePanelRoot.render(
|
|
|
|
|
h(Fragment, null, h(GraceBanner, null), h(Esp32QemuPrompt, null)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve the initial license state without blocking the SPA's mount.
|
|
|
|
|
* If there's no key OR the key doesn't authorise desktop, mount the
|
|
|
|
|
* welcome screen on top. Otherwise stay invisible — the editor takes
|
|
|
|
|
* over the window.
|
|
|
|
|
*/
|
|
|
|
|
async function checkInitialLicense(): Promise<void> {
|
|
|
|
|
if (!isTauri()) {
|
|
|
|
|
// Running outside Tauri (e.g. `vite dev` in a regular browser tab).
|
|
|
|
|
// Skip the welcome screen so the SPA is debuggable.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const key = await invoke<string | null>('license_get_key');
|
|
|
|
|
if (!key) {
|
|
|
|
|
mountWelcome();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const result = await invoke<ValidationResult>('license_validate', { key });
|
|
|
|
|
if (!result.valid || !result.entitlements?.desktop) {
|
|
|
|
|
mountWelcome();
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Network / keychain error — show welcome with the error message
|
|
|
|
|
// surfaced via the onAuthorised contract.
|
|
|
|
|
console.warn('[desktop] initial license check failed:', err);
|
|
|
|
|
mountWelcome();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const mountDesktop = (): void => {
|
|
|
|
|
if (mounted) return;
|
|
|
|
|
mounted = true;
|
2026-05-23 04:08:46 +07:00
|
|
|
dlog('mountDesktop — Tauri shell active');
|
|
|
|
|
|
|
|
|
|
// Native menubar (Velxio / File / Edit / View / Help) sends events
|
|
|
|
|
// here. Hook the listener before any UI is mounted so the first
|
|
|
|
|
// user click is never dropped.
|
|
|
|
|
void installDesktopMenuListener();
|
2026-05-21 09:50:51 +07:00
|
|
|
|
|
|
|
|
mountSidePanels();
|
|
|
|
|
void checkInitialLicense();
|
|
|
|
|
};
|