Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
3 changes: 3 additions & 0 deletions src/components/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
52 changes: 39 additions & 13 deletions src/components/OnboardingFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
},
}
);
const [accessibilitySkipped, setAccessibilitySkipped] = useLocalStorage(
"accessibilitySkipped",
false,
{
serialize: String,
deserialize: (value) => value === "true",
}
);

const {
useLocalWhisper,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand All @@ -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) {
Expand Down Expand Up @@ -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")}
/>
<PermissionCard
icon={Monitor}
Expand Down Expand Up @@ -570,9 +604,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")}
/>
<PermissionCard
icon={Monitor}
Expand Down Expand Up @@ -705,10 +739,6 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
if (!permissionsHook.micPermissionGranted) {
return false;
}
const currentPlatform = permissionsHook.pasteToolsInfo?.platform;
if (currentPlatform === "darwin") {
return permissionsHook.accessibilityPermissionGranted;
}
return true;
}

Expand Down Expand Up @@ -741,10 +771,6 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
if (!permissionsHook.micPermissionGranted) {
return false;
}
const currentPlatform = permissionsHook.pasteToolsInfo?.platform;
if (currentPlatform === "darwin") {
return permissionsHook.accessibilityPermissionGranted;
}
return true;
}
case 3:
Expand Down
9 changes: 7 additions & 2 deletions src/helpers/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ class ClipboardManager {
const platform = process.platform;
let method = "unknown";
const webContents = options.webContents;
const allowClipboardFallback = options.allowClipboardFallback === true;

try {
const shouldRestore = options.restoreClipboard !== false;
Expand All @@ -480,10 +481,14 @@ class ClipboardManager {
if (platform === "darwin") {
method = this.resolveFastPasteBinary() ? "cgevent" : "applescript";
this.safeLog("🔍 Checking accessibility permissions for paste operation...");
const hasPermissions = await this.checkAccessibilityPermissions();
const hasPermissions = await this.checkAccessibilityPermissions(allowClipboardFallback);

if (!hasPermissions) {
this.safeLog("⚠️ No accessibility permissions - text copied to clipboard only");
if (allowClipboardFallback) {
this.safeLog("✅ Clipboard fallback used (manual paste required)");
return;
}
const errorMsg =
"Accessibility permissions required for automatic pasting. Text has been copied to clipboard - please paste manually with Cmd+V.";
throw new Error(errorMsg);
Expand Down Expand Up @@ -1584,7 +1589,7 @@ Would you like to open System Settings now?`;
return;
}
if (process.platform !== "darwin") return;
this.checkAccessibilityPermissions().catch(() => {});
this.checkAccessibilityPermissions(true).catch(() => {});
this.resolveFastPasteBinary();
}

Expand Down
4 changes: 4 additions & 0 deletions src/hooks/useAudioRecording.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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?.();
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/types/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,14 @@ declare global {
interface Window {
electronAPI: {
// Basic window operations
pasteText: (text: string, options?: { fromStreaming?: boolean }) => Promise<void>;
pasteText: (
text: string,
options?: {
fromStreaming?: boolean;
restoreClipboard?: boolean;
allowClipboardFallback?: boolean;
}
) => Promise<void>;
hideWindow: () => Promise<void>;
showDictationPanel: () => Promise<void>;
onToggleDictation: (callback: () => void) => () => void;
Expand Down