fix(onboarding): require macOS Accessibility grant before first-run completes (#703)

On macOS the hotkey needs Accessibility, but first-run onboarding could finish
after Microphone only and drop users into the main UI with a dead hotkey
("nothing happens when I press the key").

Cause: desktop onboarding gated completion on the hotkey *capability* flag
(requiresAccessibilityPermission). That flag is null while the capability IPC
loads, so requiresAccessibility was briefly false — if Mic got granted in that
window, refresh() called onComplete() before Accessibility was ever requested.
App.tsx's startup gate, by contrast, always requires Accessibility, so the two
disagreed.

Fix: gate completion on the real permission status, mirroring App.tsx — macOS
returns granted/denied, every other platform returns notApplicable, so this is
timing-independent and cross-platform safe. Also surface the Accessibility step
whenever it is denied (macOS-only state) and drop the capability dependency from
its grant button, so the step always appears and is actionable even before the
capability finishes loading.

No change on Windows/Linux/Android (Accessibility = notApplicable there).

Co-authored-by: sim <sim@local>
This commit is contained in:
TRIPandsim authored and GitHub committed 2026-06-17 17:39:16 +08:00
1 parent 4903f4442e
commit f3467ae5f9
1 file changed
+9 -4
@@ -293,7 +293,12 @@ function DesktopOnboarding({
]);
setAccessibility(a);
setMicrophone(m);
const aOk = !requiresAccessibility || a === 'granted' || a === 'notApplicable';
// Gate on the real permission status — macOS returns granted/denied, other
// platforms return notApplicable. This must NOT depend on the hotkey
// capability flag: it is null while it loads, so trusting it let mic-only
// onboarding finish before we knew macOS needs Accessibility, dropping users
// into the app with a dead hotkey. Mirrors the gate in App.tsx.
const aOk = a === 'granted' || a === 'notApplicable';
const mOk = m === 'granted' || m === 'notApplicable';
if (aOk && mOk) {
onComplete();
@@ -362,7 +367,7 @@ function DesktopOnboarding({
>
<BrandHeader title={t('onboarding.welcome')} desc={t('onboarding.intro')} />
{requiresAccessibility && (
{(requiresAccessibility || accessibility === 'denied') && (
<PermissionStep
index={1}
title={capability?.requiresAccessibilityPermission ? t('onboarding.accessibilityTitle') : t('onboarding.hotkeyTitle')}
@@ -380,13 +385,13 @@ function DesktopOnboarding({
: t('onboarding.actionGrant')
}
onAction={onGrantAccessibility}
disabled={busy || !capability?.requiresAccessibilityPermission || accessibility === 'granted' || accessibility === 'notApplicable'}
disabled={busy || accessibility === 'granted' || accessibility === 'notApplicable'}
hint={capability?.requiresAccessibilityPermission ? t('onboarding.accessibilityHint') : undefined}
/>
)}
<PermissionStep
index={requiresAccessibility ? 2 : 1}
index={(requiresAccessibility || accessibility === 'denied') ? 2 : 1}
title={t('onboarding.micTitle')}
desc={t('onboarding.micDesc')}
status={microphone}