import { macSystemNames, nextMissingPermission, thisComputerNeedsPermissions, thisComputerPermissionSteps, thisComputerReady, type ThisComputerPermissionId, type ThisComputerPlatform, } from "@opengeni/sdk/this-computer"; import { CheckIcon, XIcon } from "lucide-react"; import { Dialog } from "radix-ui"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { usePortalTokenStyle } from "../../lib/use-portal-token-style"; import { ComputerIllustration, DoneIllustration, FolderPromptIllustration, SettingsRowIllustration, } from "./illustrations"; import { useOpenGeniText } from "../../messages/runtime"; import { defaultThisComputerLabels, type ThisComputerLabels } from "./labels"; import type { UseThisComputerResult } from "./use-this-computer"; /** How long a just-allowed step shows its check before the next one. */ const ALLOWED_PAUSE_MS = 1_100; /** How often the steps read the permissions while System Settings is open. */ const PERMISSION_POLL_MS = 1_000; /** After macOS's folder questions, how long the new answer takes to show. */ const FOLDER_SETTLE_MS = 700; type Phase = "intro" | "working" | "permissions" | "done"; /** Where the files step stands: not asked yet, macOS is asking, or asked. */ type FolderAsk = "idle" | "asking" | "asked"; export interface ThisComputerSetupProps { open: boolean; onOpenChange(open: boolean): void; /** From `useThisComputer` for the open workspace. */ computer: UseThisComputerResult; platform: ThisComputerPlatform; /** The app's name as System Settings lists it. */ productName: string; /** The app's icon, pictured in the System Settings row. */ appIcon?: string | null; /** The agent's mark, shown on the pictured computer (an avatar or logo). */ agentMark?: ReactNode; labels?: ThisComputerLabels; /** Where the dialog portals to; defaults to `document.body`. */ container?: HTMLElement | null; /** The person finished setting up (ready, permissions and all). */ onReady?(): void; } /** * Whether `ThisComputerSetup` is open. It opens by itself once when the * person left setup unfinished, e.g. when macOS restarted the app for a * permission, so they land back on the step they were on. */ export function useThisComputerSetupOpen( computer: UseThisComputerResult, ): [open: boolean, setOpen: (open: boolean) => void] { const [open, setOpen] = useState(false); const reopened = useRef(false); const pending = computer.status?.setupPending === true; useEffect(() => { if (!pending || reopened.current) return; reopened.current = true; setOpen(true); }, [pending]); return [open, setOpen]; } /** * Turning on "Use this computer", start to finish, in one dialog: what it * means, the one-time download, each macOS permission one at a time with a * picture of the switch to turn on, and "you're all set". Closing midway is * fine; the status pieces offer to finish later. */ export function ThisComputerSetup({ open, onOpenChange, computer, platform, productName: desktopProductName, appIcon: desktopAppIcon, agentMark, labels: given, container, onReady, }: ThisComputerSetupProps) { const catalogText = useOpenGeniText(); const labels = given ?? defaultThisComputerLabels(catalogText); const { status } = computer; const serviceBacked = status?.management === "service"; const productName = status?.permissionAppName ?? desktopProductName; const appIcon = serviceBacked ? null : desktopAppIcon; const ready = thisComputerReady(status) && !status?.reconnecting; const [phase, setPhase] = useState("intro"); const [failure, setFailure] = useState(null); const [requested, setRequested] = useState(null); const [justAllowed, setJustAllowed] = useState(null); const [folderAsk, setFolderAsk] = useState("idle"); const anchor = useRef(null); const portalStyle = usePortalTokenStyle(anchor.current); const wasOpen = useRef(false); // Each opening starts where the computer stands. useEffect(() => { if (open && !wasOpen.current) { setFailure(null); setRequested(null); setJustAllowed(null); setFolderAsk("idle"); setPhase( thisComputerNeedsPermissions(status) ? "permissions" : ready ? "done" : status?.busy ? "working" : "intro", ); } wasOpen.current = open; }, [open, status, ready]); // A download started elsewhere (or before a relaunch) finishes here. useEffect(() => { if (!open || phase !== "working" || status?.busy) return; if (thisComputerNeedsPermissions(status)) setPhase("permissions"); else if (ready) setPhase("done"); }, [open, phase, status, ready]); // Permissions are granted in System Settings, outside the app. const { refresh } = computer; useEffect(() => { if (!open || phase !== "permissions") return; const timer = window.setInterval(refresh, PERMISSION_POLL_MS); return () => window.clearInterval(timer); }, [open, phase, refresh]); const permissions = thisComputerPermissionSteps(status); const permissionsKnown = permissions.length > 0; const missing = nextMissingPermission(status); // The step shown: the one just allowed (for a moment), else the next missing. const current = justAllowed ?? missing; // When the step being shown is allowed, show its check, then move on. const shownRef = useRef(null); useEffect(() => { if (phase !== "permissions") { shownRef.current = null; return; } const shown = shownRef.current; if ( justAllowed === null && shown !== null && status?.permissions?.some((permission) => permission.id === shown && permission.granted) ) { setJustAllowed(shown); setRequested(null); return; } if (justAllowed === null && permissionsKnown) shownRef.current = missing; }, [phase, missing, justAllowed, status, permissionsKnown]); useEffect(() => { if (justAllowed === null) return; const timer = window.setTimeout(() => { shownRef.current = missing; setJustAllowed(null); if (missing === null && permissionsKnown && ready) setPhase("done"); }, ALLOWED_PAUSE_MS); return () => window.clearTimeout(timer); }, [justAllowed, missing, permissionsKnown, ready]); // Everything was already allowed (e.g. granted before the steps showed). useEffect(() => { if (phase === "permissions" && missing === null && justAllowed === null && status) { if (status.state === "connected" && !status.reconnecting && permissionsKnown) setPhase("done"); } }, [phase, missing, justAllowed, status, permissionsKnown]); const readyNotified = useRef(false); useEffect(() => { if (phase === "done" && open && !readyNotified.current) { readyNotified.current = true; onReady?.(); } if (phase !== "done") readyNotified.current = false; }, [phase, open, onReady]); async function turnOn() { setFailure(null); setPhase("working"); const next = await computer.connect({ screenControl: true }); if (!next || next.state !== "connected") { setFailure( labels.error(next?.errorCode ?? null, next?.error ?? computer.actionError ?? null), ); setPhase("intro"); return; } setPhase( thisComputerNeedsPermissions(next) ? "permissions" : next.reconnecting ? "working" : "done", ); } const machineName = status?.machineName ?? ""; const close = () => onOpenChange(false); const busyConnecting = phase === "working"; let lead: ReactNode = null; let body: ReactNode = null; let title: string; let description: string; let footer: ReactNode; if (phase === "done") { title = labels.doneTitle; description = labels.done(machineName, platform); lead = ; body = labels.tryThis ?

{labels.tryThis}

: null; footer = ( ); } else if (phase === "permissions" && current && permissionsKnown) { const index = Math.max( 0, permissions.findIndex((permission) => permission.id === current), ); const step = labels.permission(current, productName); // What's on screen is in the system's language, which can differ from ours. const system = computer.status?.systemNames ?? macSystemNames(typeof navigator === "undefined" ? [] : navigator.languages); const allowed = justAllowed === current; const waiting = requested === current && !allowed; const folders = current === "files"; // Asked, and still not allowed: a folder was refused, so System Settings is the way. const refused = folders && folderAsk === "asked" && !allowed; title = step.title; description = step.why; lead = ( <>
    {permissions.map((permission, position) => (
  1. ))}

{labels.stepOf(index + 1, permissions.length)}

); body = ( <> {folders && !refused ? ( ) : ( )}
{allowed ? ( {labels.allowed} ) : refused ? ( {labels.foldersRefused(productName)} ) : folders ? ( {folderAsk === "asking" ? : null} {labels.waitingForFolders(system.allow)} ) : waiting ? ( {labels.waitingForSwitch(productName, system.openSettings)} ) : ( {labels.passwordHint} )}
{serviceBacked ? ( <>

{catalogText("thisComputer.servicePermissions")}

) : waiting && (current === "screenRecording" || current === "inputMonitoring") ? (

{labels.quitHint(productName, system.later)}

) : null} ); const askFolders = async () => { setRequested("files"); setFolderAsk("asking"); await computer.requestPermission("files"); window.setTimeout(() => setFolderAsk("asked"), FOLDER_SETTLE_MS); }; footer = ( <> {folders && !refused ? ( ) : ( )} ); } else if (phase === "working" || (phase === "permissions" && !permissionsKnown)) { const progress = status?.setupProgress; title = status?.management === "service" ? catalogText("thisComputer.reconnecting", { machineName }) : labels.settingUp(machineName); description = status?.management === "service" ? catalogText("thisComputer.serviceConnecting") : labels.settingUpDetail; lead = ; body = ( <>
); footer = null; } else { title = labels.title; description = labels.intro(machineName); lead = ; body = ( <>
    {labels.abilities.map((ability) => (
  • {ability}
  • ))}

{serviceBacked ? catalogText("thisComputer.existingService") : labels.safety(platform)}

{failure ? (

{failure}

) : null} ); footer = ( <> ); } return ( <>