From 983820cee35487f80e54a96100a4b90513b4c5a3 Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Tue, 3 Mar 2026 17:33:23 +0700 Subject: [PATCH] fix: improve permission onboarding UX for app updates When the binary changes (e.g. after an update), macOS revokes accessibility permissions. The onboarding screen previously got stuck in "Waiting..." with no way out. - Add Retry button to re-check permissions on demand - Add restart hint when stuck in waiting state - Add "Continue anyway" link for returning users (skip to main app) - Pass isReturningUser prop from App.tsx to AccessibilityOnboarding --- src/App.tsx | 7 +- .../onboarding/AccessibilityOnboarding.tsx | 86 +++++++++++++++++-- src/i18n/locales/en/translation.json | 3 + 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cf96296..873d084 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -151,7 +151,12 @@ function App() { } if (onboardingStep === "accessibility") { - return ; + return ( + + ); } if (onboardingStep === "model") { diff --git a/src/components/onboarding/AccessibilityOnboarding.tsx b/src/components/onboarding/AccessibilityOnboarding.tsx index 0a4e52f..f473c37 100644 --- a/src/components/onboarding/AccessibilityOnboarding.tsx +++ b/src/components/onboarding/AccessibilityOnboarding.tsx @@ -11,10 +11,11 @@ import { toast } from "sonner"; import { commands } from "@/bindings"; import { useSettingsStore } from "@/stores/settingsStore"; import DictxTextLogo from "../icons/DictxTextLogo"; -import { Keyboard, Mic, Check, Loader2 } from "lucide-react"; +import { Keyboard, Mic, Check, Loader2, RefreshCw } from "lucide-react"; interface AccessibilityOnboardingProps { onComplete: () => void; + isReturningUser?: boolean; } type PermissionStatus = "checking" | "needed" | "waiting" | "granted"; @@ -26,6 +27,7 @@ interface PermissionsState { const AccessibilityOnboarding: React.FC = ({ onComplete, + isReturningUser = false, }) => { const { t } = useTranslation(); const refreshAudioDevices = useSettingsStore( @@ -200,6 +202,39 @@ const AccessibilityOnboarding: React.FC = ({ } }; + const handleRetryCheck = async () => { + try { + const [accessibilityGranted, microphoneGranted] = await Promise.all([ + checkAccessibilityPermission(), + checkMicrophonePermission(), + ]); + + if (accessibilityGranted) { + await Promise.all([ + commands.initializeEnigo(), + commands.initializeShortcuts(), + ]).catch((e) => + console.warn("Failed to initialize after permission grant:", e), + ); + } + + setPermissions({ + accessibility: accessibilityGranted ? "granted" : "waiting", + microphone: microphoneGranted ? "granted" : "waiting", + }); + + if (accessibilityGranted && microphoneGranted) { + await Promise.all([refreshAudioDevices(), refreshOutputDevices()]); + timeoutRef.current = setTimeout(() => onComplete(), 300); + } else { + startPolling(); + } + } catch (error) { + console.error("Failed to retry permission check:", error); + toast.error(t("onboarding.permissions.errors.checkFailed")); + } + }; + // Still checking platform/initial permissions if ( isMacOS === null || @@ -263,9 +298,18 @@ const AccessibilityOnboarding: React.FC = ({ {t("onboarding.permissions.granted")} ) : permissions.microphone === "waiting" ? ( -
- - {t("onboarding.permissions.waiting")} +
+
+ + {t("onboarding.permissions.waiting")} +
+
) : (
) : permissions.accessibility === "waiting" ? ( -
- - {t("onboarding.permissions.waiting")} +
+
+ + {t("onboarding.permissions.waiting")} +
+
) : (
+ + {/* Restart hint */} + {(permissions.accessibility === "waiting" || + permissions.microphone === "waiting") && ( +

+ {t("onboarding.permissions.restartHint")} +

+ )} + + {/* Continue anyway for returning users */} + {isReturningUser && ( + + )} ); diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 59496fe..da0f9db 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -101,6 +101,9 @@ "grant": "Grant Permission", "granted": "Granted", "waiting": "Waiting...", + "retry": "Retry", + "restartHint": "If stuck, try toggling the permission off and on in System Settings, then click Retry.", + "skipForNow": "Continue without permissions — I'll fix this later", "allGranted": "All set!", "errors": { "checkFailed": "Failed to check permissions. Please try again.",