diff --git a/src/App.jsx b/src/App.jsx index 7bc72af4..556b24db 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -114,6 +114,9 @@ export default function App() { }); const unsubscribeAccessibility = window.electronAPI?.onAccessibilityMissing?.(() => { + if (localStorage.getItem("accessibilitySkipped") === "true") { + return; + } toast({ title: t("app.toasts.accessibilityMissing.title"), description: t("app.toasts.accessibilityMissing.description"), diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index 7825b70c..c5d059eb 100644 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -217,6 +217,9 @@ export default function ControlPanel() { // When accessibility is missing on macOS, open the permissions settings page useEffect(() => { const cleanup = window.electronAPI?.onAccessibilityMissing?.(() => { + if (localStorage.getItem("accessibilitySkipped") === "true") { + return; + } setSettingsSection("privacyData"); setShowSettings(true); toast({ diff --git a/src/components/OnboardingFlow.tsx b/src/components/OnboardingFlow.tsx index c250c003..d1931a81 100644 --- a/src/components/OnboardingFlow.tsx +++ b/src/components/OnboardingFlow.tsx @@ -71,6 +71,14 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { }, } ); + const [accessibilitySkipped, setAccessibilitySkipped] = useLocalStorage( + "accessibilitySkipped", + false, + { + serialize: String, + deserialize: (value) => value === "true", + } + ); const { useLocalWhisper, @@ -128,6 +136,16 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { const screenRecording = useScreenRecordingPermission(); + useEffect(() => { + if (permissionsHook.accessibilityPermissionGranted && accessibilitySkipped) { + setAccessibilitySkipped(false); + } + }, [ + permissionsHook.accessibilityPermissionGranted, + accessibilitySkipped, + setAccessibilitySkipped, + ]); + // For signed-in users, merge setup and permissions into one step const steps = isSignedIn && !skipAuth @@ -296,6 +314,13 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { return; } + const isMacOS = getPlatform() === "darwin"; + const isPermissionsStep = + isSignedIn && !skipAuth ? currentStep === 1 : currentStep === 2; + if (isMacOS && isPermissionsStep && !permissionsHook.accessibilityPermissionGranted) { + setAccessibilitySkipped(true); + } + const newStep = currentStep + 1; setCurrentStep(newStep); @@ -305,7 +330,16 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { window.electronAPI.showDictationPanel(); } } - }, [currentStep, setCurrentStep, steps.length, activationStepIndex]); + }, [ + currentStep, + setCurrentStep, + steps.length, + activationStepIndex, + isSignedIn, + skipAuth, + permissionsHook.accessibilityPermissionGranted, + setAccessibilitySkipped, + ]); const prevStep = useCallback(() => { if (currentStep > 0) { @@ -409,9 +443,9 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) { description={t("onboarding.permissions.accessibilityDescription")} granted={permissionsHook.accessibilityPermissionGranted} onRequest={permissionsHook.testAccessibilityPermission} - buttonText={t("onboarding.permissions.testAndGrant")} + buttonText={t("onboarding.permissions.grant")} onOpenSettings={permissionsHook.openAccessibilitySettings} - openSettingsText={t("onboarding.permissions.openSystemSettings")} + badge={t("onboarding.permissions.optional")} /> {}); + this.checkAccessibilityPermissions(true).catch(() => {}); this.resolveFastPasteBinary(); } diff --git a/src/hooks/useAudioRecording.js b/src/hooks/useAudioRecording.js index 774589b1..7334f1a4 100644 --- a/src/hooks/useAudioRecording.js +++ b/src/hooks/useAudioRecording.js @@ -113,10 +113,14 @@ export const useAudioRecording = (toast, options = {}) => { const isStreaming = result.source?.includes("streaming"); const { keepTranscriptionInClipboard } = getSettings(); + const accessibilitySkipped = + typeof window !== "undefined" && + window.localStorage.getItem("accessibilitySkipped") === "true"; const pasteStart = performance.now(); await audioManagerRef.current.safePaste(result.text, { ...(isStreaming ? { fromStreaming: true } : {}), restoreClipboard: !keepTranscriptionInClipboard, + allowClipboardFallback: accessibilitySkipped, }); logger.info( "Paste timing", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 9cfa4475..5e2b9bd2 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -157,7 +157,7 @@ "accessibilityTitle": "Accessibility", "accessibilityDescription": "Enable in System Settings by toggling it on", "testAndGrant": "Test & Grant", - "requiredForApp": "Required for OpenWhispr to work", + "requiredForApp": "Microphone required. Accessibility enables auto-paste.", "microphoneRequired": "Microphone access required", "openSystemSettings": "Open System Settings", "screenRecordingTitle": "System Audio", diff --git a/src/main.jsx b/src/main.jsx index 777fa35d..340d0b45 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -314,6 +314,7 @@ function MainApp() { const authSkipped = localStorage.getItem("authenticationSkipped") === "true" || localStorage.getItem("skipAuth") === "true"; + const accessibilitySkipped = localStorage.getItem("accessibilitySkipped") === "true"; // Valid session proves prior onboarding — restore flag if localStorage was wiped const isReturningUser = !onboardingCompleted && isSignedIn; @@ -332,7 +333,7 @@ function MainApp() { // Returning users who skipped onboarding may lack accessibility permissions. // Trigger an immediate check so the main process sends accessibility-missing. - if (isReturningUser) { + if (isReturningUser && !accessibilitySkipped) { window.electronAPI?.checkAccessibilityTrusted?.(); } } diff --git a/src/types/electron.ts b/src/types/electron.ts index 4acdfd90..e5e01529 100644 --- a/src/types/electron.ts +++ b/src/types/electron.ts @@ -275,7 +275,14 @@ declare global { interface Window { electronAPI: { // Basic window operations - pasteText: (text: string, options?: { fromStreaming?: boolean }) => Promise; + pasteText: ( + text: string, + options?: { + fromStreaming?: boolean; + restoreClipboard?: boolean; + allowClipboardFallback?: boolean; + } + ) => Promise; hideWindow: () => Promise; showDictationPanel: () => Promise; onToggleDictation: (callback: () => void) => () => void;