import { IconCircleCheck, IconCircleOff } from "@tabler/icons-react"; import { invoke } from "@tauri-apps/api/core"; import { open as openExternal } from "@tauri-apps/plugin-shell"; import { useEffect, useState } from "react"; import { isMacPlatform } from "../lib/platform"; type PermissionPane = | "microphone" | "speech" | "accessibility" | "input-monitoring"; type PermissionStatuses = { screen: boolean; camera: boolean; microphone: boolean; speech: boolean; accessibility: boolean; inputMonitoring: boolean; }; const PERMISSION_CARDS: Array<{ pane: PermissionPane; key: keyof PermissionStatuses; name: string; desc: string; }> = [ { pane: "microphone", key: "microphone", name: "Microphone", desc: "Needed to hear you for dictation and meeting transcripts.", }, { pane: "speech", key: "speech", name: "Speech Recognition", desc: "Turns your voice into text on-device.", }, { pane: "accessibility", key: "accessibility", name: "Accessibility", desc: "Lets Clips paste dictated text into other apps.", }, { pane: "input-monitoring", key: "inputMonitoring", name: "Input Monitoring", desc: "Only needed for the hold-Fn dictation shortcut.", }, ]; // Fallback deep-links when the native `open_macos_privacy_settings` command // fails — same URLs the main app window uses. const MACOS_PRIVACY_URLS: Record = { microphone: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone", speech: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_SpeechRecognition", accessibility: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility", "input-monitoring": "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent", }; function openPermissionSettings(pane: PermissionPane): void { invoke("open_macos_privacy_settings", { pane }).catch(() => { openExternal(MACOS_PRIVACY_URLS[pane]).catch((err) => { console.error("[onboarding] open privacy settings failed", err); }); }); } /** * First-launch overlay. Full-screen with a solid dark background — NOT * transparent like countdown/finalizing. * * Two steps: * 1. Feature selection — three cards with checkboxes (all checked by * default). * 2. Permissions (macOS, when Voice Dictation or Meetings is enabled) — * live permission-status cards polled every 2s so they flip to granted * as the user works through System Settings. Never blocks: the primary * button always continues. * * Finishing calls `set_feature_config` with the chosen features + * `onboardingComplete: true`, then opens the popover via `show_popover`. */ export function Onboarding() { const [clips, setClips] = useState(true); const [meetings, setMeetings] = useState(true); const [voice, setVoice] = useState(true); const [submitting, setSubmitting] = useState(false); const [step, setStep] = useState<"features" | "permissions">("features"); const [statuses, setStatuses] = useState(null); const needsPermissionsStep = (voice || meetings) && isMacPlatform(); useEffect(() => { if (step !== "permissions") return; let cancelled = false; const check = () => { invoke("check_permission_statuses") .then((next) => { if (!cancelled) setStatuses(next); }) .catch(() => { // Command unavailable — leave statuses null (cards show no status). }); }; check(); const id = setInterval(check, 2_000); return () => { cancelled = true; clearInterval(id); }; }, [step]); async function finish() { if (submitting) return; setSubmitting(true); try { await invoke("set_feature_config", { config: { clipsEnabled: clips, meetingsEnabled: meetings, voiceEnabled: voice, launchAtLoginEnabled: true, autoHidePopoverEnabled: false, meetingTranscriptionMode: "ask", showMeetingWidgetEnabled: true, showInScreenCapture: false, onboardingComplete: true, }, }); await invoke("show_popover"); // Close the onboarding window itself — show_popover only opens the // popover, it doesn't know an onboarding window exists to dismiss. await invoke("hide_onboarding_window"); } catch (err) { console.error( "[onboarding] set_feature_config / show_popover failed", err, ); setSubmitting(false); } } function handleFeaturesContinue() { if (needsPermissionsStep) { setStep("permissions"); return; } void finish(); } if (step === "permissions") { const allGranted = statuses !== null && PERMISSION_CARDS.every((card) => statuses[card.key]); return (

Permissions

Grant these in System Settings — Clips updates automatically

{PERMISSION_CARDS.map((card) => { const granted = statuses ? statuses[card.key] : null; return (
{granted ? ( ) : ( )}
{card.name} {card.desc}
{!granted ? ( ) : null}
); })}
); } return (

Welcome to Clips

Choose your features

); }