fix(call): show mic-denied error before joining instead of crashing

Check navigator.permissions for microphone state before the call starts.
If the user has blocked microphone access, disable the Join button and
show an inline message explaining how to fix it in browser settings.
Subscribes to permission change events so the UI updates if they grant
access without refreshing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-05-14 11:56:38 -04:00
parent 2384672a53
commit 9580f2a744
+33 -3
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Box, Button, Icon, Icons, Spinner, Text } from 'folds';
import { SequenceCard } from '../../components/sequence-card';
import * as css from './styles.css';
@@ -7,6 +7,28 @@ import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
import { useCallEmbed, useCallJoined, useCallStart } from '../../hooks/useCallEmbed';
import { useCallPreferences } from '../../state/hooks/callPreferences';
type MediaPermState = 'granted' | 'denied' | 'prompt' | 'unknown';
function useMediaPermissions(): MediaPermState {
const [state, setState] = useState<MediaPermState>('unknown');
useEffect(() => {
if (!navigator.permissions) {
setState('unknown');
return;
}
navigator.permissions
.query({ name: 'microphone' as PermissionName })
.then((result) => {
setState(result.state as MediaPermState);
result.onchange = () => setState(result.state as MediaPermState);
})
.catch(() => setState('unknown'));
}, []);
return state;
}
type PrescreenControlsProps = {
canJoin?: boolean;
};
@@ -21,7 +43,10 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
const startCall = useCallStart(direct);
const joining = callEmbed?.roomId === room.roomId && !callJoined;
const disabled = inOtherCall || !canJoin;
const micPermission = useMediaPermissions();
const micDenied = micPermission === 'denied';
const disabled = inOtherCall || !canJoin || micDenied;
const { microphone, video, sound, toggleMicrophone, toggleVideo, toggleSound } =
useCallPreferences();
@@ -45,7 +70,12 @@ export function PrescreenControls({ canJoin }: PrescreenControlsProps) {
<VideoButton enabled={video} onToggle={toggleVideo} />
<ChatButton />
</Box>
<Box grow="Yes" direction="Column">
<Box grow="Yes" direction="Column" gap="200">
{micDenied && (
<Text size="T200" style={{ color: 'var(--tc-critical-high, #e53e3e)', textAlign: 'center' }}>
Microphone access is blocked. Enable it in your browser settings to join.
</Text>
)}
<Button
variant={disabled ? 'Secondary' : 'Success'}
fill={disabled ? 'Soft' : 'Solid'}