{"record":{"id":"8e41b6a451573e8d","repo":"stablyai/orca","slug":"microphone-permission-denied","errorCode":null,"errorMessage":"Microphone permission denied","messagePattern":"Microphone permission denied","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mobile/src/hooks/use-mobile-dictation.ts","lineNumber":132,"sourceCode":"    const client = clientRef.current\n    if (!client || !enabledRef.current || activeIdRef.current) {\n      return\n    }\n\n    const generation = generationRef.current + 1\n    generationRef.current = generation\n    setError(null)\n    setStatus('starting')\n    const permission = await requestMicrophonePermissionsAsync()\n    if (generationRef.current !== generation || !enabledRef.current) {\n      if (generationRef.current === generation) {\n        setStatus('idle')\n      }\n      return\n    }\n    if (!permission.granted) {\n      setStatus('idle')\n      throw new Error('Microphone permission denied')\n    }\n\n    const initialized = await initialize()\n    if (generationRef.current !== generation || !enabledRef.current) {\n      void tearDown()\n      if (generationRef.current === generation) {\n        setStatus('idle')\n      }\n      return\n    }\n    if (!initialized) {\n      setStatus('idle')\n      throw new Error('Failed to initialize microphone')\n    }\n\n    const dictationId = createMobileDictationId()\n    activeIdRef.current = dictationId\n","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/mobile/src/hooks/use-mobile-dictation.ts#L114-L150","documentation":"Thrown by the start callback in use-mobile-dictation after requestMicrophonePermissionsAsync() returned a result with permission.granted === false. This is the user-level permission gate — the OS-level mic permission was denied (or restricted), so the hook refuses to proceed to initialize()/start.","triggerScenarios":"await requestMicrophonePermissionsAsync() resolves with {granted:false}. Reached only after the generation/enabled staleness check passes. Causes: user tapped 'Don't Allow' on the OS permission prompt, permission is restricted by MDM/screen-time policy, the prompt was previously denied and iOS now returns .denied without a prompt.","commonSituations":"First-time user denied the prompt; user previously denied and is retrying (iOS now needs Settings toggle); enterprise MDM blocks mic; Android permission was revoked in system settings while the app was backgrounded.","solutions":["Surface a deep-link to the OS settings page so the user can re-enable the microphone permission (iOS: app settings, Android: package settings).","Distinguish 'denied' from 'blocked'/restricted and tailor the copy — a restricted state needs an admin/MDM fix, not a settings toggle.","Re-request only when the user explicitly taps a 'request again' affordance; do not loop requestMicrophonePermissionsAsync on mount.","After the user returns from settings, re-run start() — the permission will now report granted."],"exampleFix":"// before\nconst permission = await requestMicrophonePermissionsAsync()\nif (!permission.granted) {\n  setStatus('idle')\n  throw new Error('Microphone permission denied')\n}\n\n// after — branch on the denial reason so the UI can deep-link settings\nconst permission = await requestMicrophonePermissionsAsync()\nif (!permission.granted) {\n  setStatus('idle')\n  const reason = permission.canRequestAgain === false ? 'blocked' : 'denied'\n  throw new Error(`Microphone permission ${reason}`)\n}","handlingStrategy":"validation","validationCode":"// Check current permission status without prompting first\nconst status = await getMicrophonePermissionStatusAsync()\nif (status !== 'granted') {\n  openAppSettings()\n  return\n}","typeGuard":"function isMicrophonePermissionError(err: unknown): boolean {\n  return err instanceof Error && err.message === 'Microphone permission denied'\n}","tryCatchPattern":"try {\n  await start()\n} catch (err) {\n  if (isMicrophonePermissionError(err)) {\n    showPermissionDeniedUI({ onOpenSettings: openAppSettings })\n    return\n  }\n  throw err\n}","preventionTips":["Do not auto-prompt on mount; prompt only when the user taps the mic affordance.","Deep-link to OS settings when canRequestAgain is false (previously denied).","Distinguish 'denied' from 'blocked'/restricted so MDM-restricted users get the right guidance."],"tags":["dictation","permissions","microphone","speech","react-native"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}