Skip to content

fix: Microphone permission never prompted on first launch - #42

Merged
jatinkrmalik merged 3 commits into
mainfrom
fix/mic-permission-flow
Mar 6, 2026
Merged

fix: Microphone permission never prompted on first launch#42
jatinkrmalik merged 3 commits into
mainfrom
fix/mic-permission-flow

Conversation

@jatinkrmalik

Copy link
Copy Markdown
Member

Problem

On first launch via DMG, the microphone permission prompt is never shown. The app immediately marks mic permission as denied and logs it that way — without ever asking the user. Once in this state, there's no way to re-trigger the system prompt.

Root Cause

Two bugs working together:

  1. AudioEngine.checkPermission() treated .notDetermined as .denied — Both cases returned false, so AppState could never distinguish a first launch from an explicit denial.

  2. performStartup() never called requestMicrophonePermission() — It only called checkPermissions(), which silently marked mic as denied without triggering the system prompt.

Fix

AudioEngine

  • Replaced boolean checkPermission(completion:) with tri-state checkPermissionStatus() -> PermissionStatus that properly returns .notDetermined, .granted, or .denied

AppState

  • checkPermissions() now preserves .notDetermined state
  • performStartup() auto-calls requestMicrophonePermission() when status is .notDetermined — this triggers the system prompt on first launch
  • requestMicrophonePermission() opens System Settings when already .denied (re-requesting won't show the prompt again)
  • Added openMicrophoneSettings() helper that opens the Microphone privacy pane directly

Settings UI

  • PermissionRow now shows three states:
    • Granted (green checkmark)
    • Not Determined (orange question mark + "Grant" button)
    • Denied (red X + "Open Settings" button)
  • Help text shown when any permission is denied, guiding user to System Settings

Popover

  • Permission buttons adapt labels and colors based on state
  • Denied mic shows "Open Microphone Settings" (red) instead of "Grant Microphone Access" (orange)
  • Denied state shows specific guidance text

Root cause: Two bugs working together:
1. AudioEngine.checkPermission() treated .notDetermined as .denied
   (both returned false), so AppState could never distinguish first
   launch from an explicit denial.
2. performStartup() called checkPermissions() but never called
   requestMicrophonePermission(), so the system prompt was never
   triggered.

Changes:
- AudioEngine: Replace boolean checkPermission() with tri-state
  checkPermissionStatus() returning PermissionStatus (.notDetermined,
  .granted, .denied)
- AppState: checkPermissions() now preserves .notDetermined state;
  performStartup() auto-prompts for mic permission when notDetermined;
  requestMicrophonePermission() opens System Settings when denied
  (re-requesting won't show the prompt again)
- SettingsView: PermissionRow shows 'Grant' (orange) for notDetermined,
  'Open Settings' (red) for denied, with help text for denied state
- MenuBarView: Popover permission buttons adapt labels and colors
  based on denied vs notDetermined state
Always create a fresh temporary event tap when checking Input Monitoring
permission. The previous approach checked the existing HotKeyManager tap,
but macOS doesn't immediately disable existing taps when the user revokes
Input Monitoring — the change only takes effect on app restart. A fresh
tap creation always reflects the current permission state.
Use .cghidEventTap (same tap type as HotKeyManager) instead of
.cgSessionEventTap for permission checks and auto-registration.
.cgSessionEventTap can inherit Terminal's permissions when the app
is launched from CLI, giving false positives.

Also restore Strategy 1: check if HotKeyManager's existing tap has been
disabled by macOS (which happens on permission revocation).
@jatinkrmalik
jatinkrmalik merged commit 6b2974b into main Mar 6, 2026
1 check passed
@jatinkrmalik
jatinkrmalik deleted the fix/mic-permission-flow branch August 22, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant