Microphone Permissions
Comprehensive solution for managing microphone permissions in voice-enabled web applications
The useMicrophonePermission
hook provides a robust foundation for handling microphone permissions in voice-enabled applications. This guide covers implementation patterns, best practices, and complete examples for seamless voice interaction.
Core Concepts
Permission States
The hook manages six distinct permission states that cover all possible scenarios:
Permission Flow Visualization
The permission flow follows a predictable pattern that ensures smooth user experience:
Hook API Reference
Basic Usage
Import and use the hook to manage microphone permissions in your components:
import { useMicrophonePermission } from '@sammy-labs/sammy-three' ;
const MyComponent = () => {
const {
state , // Current permission state
error , // Error message if any
stream , // MediaStream when granted
isPermissionGranted , // Boolean: permission granted
isPermissionDenied , // Boolean: permission denied
needsPermission , // Boolean: needs user action
checkPermission , // Check current permission
checkPermissionWithPolling , // Check with auto-retry
request , // Request permission
refresh , // Refresh permission state
stop // Stop stream and cleanup
} = useMicrophonePermission ({
onMicrophonePermissionRequired : () => {
// Callback when permission is needed
console . log ( 'Microphone permission required' );
}
});
};
Return Values
state
PermissionStateType
required
Current permission state. One of: idle
, prompt
, granted
, denied
, error
, unsupported
Error message when state is error
. Null otherwise.
Active media stream when permission is granted. Null otherwise.
Convenience boolean that’s true when state is granted
.
Convenience boolean that’s true when state is denied
.
Indicates whether user action is needed to grant permission.
checkPermission
() => Promise<PermissionCheckResult>
Checks current permission status without requesting access.
checkPermissionWithPolling
(callback?) => Promise<boolean>
Checks permission with automatic retry logic. Useful for waiting for user action.
Requests microphone permission from the browser.
Refreshes the current permission state.
Stops the media stream and performs cleanup.
Implementation Patterns
Modal-Based Flow
Auto Polling
Inline Check
Pattern 1: Modal-Based Permission Flow (Recommended) This is the recommended pattern for guide/walkthrough scenarios where you want full control over the user experience. MicrophonePermissionModal.tsx
modal-styles.css
import React , { useEffect , useState } from 'react' ;
import {
useMicrophonePermission ,
useSammyAgentContext ,
AgentMode
} from '@sammy-labs/sammy-three' ;
export const MicrophonePermissionModal = ({ open , onOpenChange }) => {
const [ isRequesting , setIsRequesting ] = useState ( false );
const { startAgent , guides } = useSammyAgentContext ();
const {
state ,
error ,
isPermissionGranted ,
isPermissionDenied ,
checkPermission ,
request ,
refresh
} = useMicrophonePermission ();
// Initial permission check
useEffect (() => {
if ( open ) {
checkPermission ();
}
}, [ open , checkPermission ]);
// Auto-start agent when permission is granted
useEffect (() => {
if ( isPermissionGranted && open ) {
handleStartAgent ();
}
}, [ isPermissionGranted , open ]);
const handleStartAgent = async () => {
try {
await startAgent ({
agentMode: AgentMode . USER ,
guideId: guides ?. currentGuide ?. guideId ,
});
onOpenChange ( false ); // Close modal after successful start
} catch ( error ) {
console . error ( 'Failed to start agent:' , error );
}
};
const handleRequestPermission = async () => {
setIsRequesting ( true );
try {
await request ();
// If successful, the useEffect above will handle starting the agent
} finally {
setIsRequesting ( false );
}
};
if ( ! open ) return null ;
return (
< div className = "modal-overlay" >
< div className = "modal-content" >
{ renderContent ()}
</ div >
</ div >
);
function renderContent () {
// Permission already granted - auto-starting
if ( isPermissionGranted ) {
return (
< div className = "permission-granted" >
< h3 > Starting Voice Assistant ...</ h3 >
< div className = "spinner" />
</ div >
);
}
// Permission denied - show instructions
if ( isPermissionDenied ) {
return (
< div className = "permission-denied" >
< h3 >🚫 Microphone Access Blocked </ h3 >
< p > To use voice features , you need to grant microphone access : </ p >
< ol >
< li > Click the lock icon in your browser 's address bar</li >
< li > Find "Microphone" settings </ li >
< li > Change from "Block" to "Allow" </ li >
< li > Click "Check Again" below </ li >
</ ol >
< button onClick = { refresh } > 🔄 Check Again </ button >
</ div >
);
}
// Error state
if ( state === 'error' && error ) {
return (
< div className = "permission-error" >
< h3 >⚠️ Microphone Error </ h3 >
< p >{ error } </ p >
< button onClick = { refresh } > Try Again </ button >
</ div >
);
}
// Default: Need to request permission
return (
< div className = "permission-prompt" >
< h3 >🎤 Enable Voice Assistant ? </ h3 >
< p > Our AI assistant will guide you through the platform using voice . </ p >
< p > Click "Enable Microphone" and allow access when prompted . </ p >
< button
onClick = { handleRequestPermission }
disabled = { isRequesting }
>
{isRequesting ? 'Requesting...' : '🎤 Enable Microphone' }
</ button >
</ div >
);
}
};
UI/UX Best Practices
Clear Permission Context
Explain the Why Always explain WHY you need microphone access before requesting. Users are more likely to grant permissions when they understand the value. Permission Context Example
< div className = "permission-context" >
< h3 > Voice-Guided Experience </ h3 >
< ul >
< li > ✓ Real-time voice guidance through the platform </ li >
< li > ✓ Answer your questions as you navigate </ li >
< li > ✓ Hands-free interaction </ li >
</ ul >
< p > We only access your microphone during active sessions. </ p >
</ div >
Progressive Disclosure
Wait for User Intent Don’t request permissions immediately on page load. Wait for explicit user action to avoid feeling intrusive. Progressive Disclosure Example
export const GuidedTour = () => {
const [ showPermissionModal , setShowPermissionModal ] = useState ( false );
return (
<>
< button onClick = { () => setShowPermissionModal ( true ) } >
Start Guided Tour
</ button >
{ showPermissionModal && (
< MicrophonePermissionModal
open = { showPermissionModal }
onOpenChange = { setShowPermissionModal }
/>
) }
</>
);
};
State-Specific Messaging
Provide clear, actionable messages for each permission state:
const getStateMessage = ( state : PermissionStateType , error ?: string ) => {
switch ( state ) {
case 'prompt' :
return {
title: 'Enable Microphone Access' ,
message: 'Click below to enable your microphone for voice guidance.' ,
action: 'Enable Microphone'
};
case 'denied' :
return {
title: 'Microphone Access Blocked' ,
message: 'You \' ll need to update your browser settings to continue.' ,
action: 'View Instructions'
};
case 'error' :
return {
title: 'Microphone Error' ,
message: error || 'Unable to access microphone' ,
action: 'Try Again'
};
case 'granted' :
return {
title: 'Ready to Start' ,
message: 'Microphone access granted!' ,
action: null
};
default :
return {
title: 'Checking Permissions' ,
message: 'Please wait...' ,
action: null
};
}
};
Error Handling
Common Error Scenarios
Handle different error types with specific solutions:
const handleMicrophoneError = ( error : string ) => {
const errorHandlers = {
'No microphone device found.' : {
icon: '🎤' ,
solution: 'Please connect a microphone to your device.' ,
canRetry: true
},
'Microphone is already in use by another application.' : {
icon: '⚠️' ,
solution: 'Close other apps using your microphone (Zoom, Teams, etc.)' ,
canRetry: true
},
'Microphone permission was denied.' : {
icon: '🚫' ,
solution: 'Update browser settings to allow microphone access.' ,
canRetry: false ,
showInstructions: true
}
};
const handler = errorHandlers [ error ] || {
icon: '❌' ,
solution: 'Please try again or contact support.' ,
canRetry: true
};
return handler ;
};
Graceful Degradation
Always provide fallback options when voice isn’t available:
export const AdaptiveGuide = () => {
const { state } = useMicrophonePermission ();
const isVoiceAvailable = state === 'granted' ;
return (
< div className = "guide-container" >
{ isVoiceAvailable ? (
< VoiceGuidedExperience />
) : (
< TextBasedGuide
showVoicePrompt = { state === 'prompt' }
onEnableVoice = {() => /* handle permission request */ }
/>
)}
</ div >
);
};
Complete Integration Example
Here’s a comprehensive implementation that combines all the best practices:
SammyStartWalkthroughModal.tsx
styles.css
/**
* Sammy Start Walkthrough Modal
* Modal that automatically appears when a guide is available
* Handles microphone permission flow seamlessly
*/
import React , { useEffect , useState , useCallback } from 'react' ;
import {
useMicrophonePermission ,
useSammyAgentContext ,
AgentMode
} from '@sammy-labs/sammy-three' ;
export const SammyStartWalkthroughModal = () => {
const [ isModalOpen , setIsModalOpen ] = useState ( false );
const [ hasStartedWalkthrough , setHasStartedWalkthrough ] = useState ( false );
const [ isStarting , setIsStarting ] = useState ( false );
const { activeSession , startAgent , guides } = useSammyAgentContext ();
const {
state ,
error ,
isPermissionGranted ,
isPermissionDenied ,
needsPermission ,
checkPermission ,
request ,
refresh
} = useMicrophonePermission ();
const startWalkthroughSession = useCallback ( async () => {
try {
setHasStartedWalkthrough ( true );
await startAgent ({
agentMode: AgentMode . USER ,
guideId: guides ?. currentGuide ?. guideId ,
});
setIsModalOpen ( false );
setIsStarting ( false );
} catch ( error ) {
console . error ( 'Error starting walkthrough:' , error );
setIsStarting ( false );
// Handle error appropriately
}
}, [ startAgent , guides ?. currentGuide ?. guideId ]);
// Open modal immediately when guide is available but session isn't active
useEffect (() => {
if ( guides ?. currentGuide && ! activeSession && ! hasStartedWalkthrough ) {
setIsModalOpen ( true );
checkPermission (); // Pre-check permission
}
}, [ guides ?. currentGuide , activeSession , hasStartedWalkthrough , checkPermission ]);
// Auto-start when permission is granted while modal is open and starting
useEffect (() => {
if ( isPermissionGranted && isModalOpen && isStarting ) {
startWalkthroughSession ();
}
}, [ isPermissionGranted , isModalOpen , isStarting , startWalkthroughSession ]);
const handleStartWalkthrough = async () => {
setIsStarting ( true );
try {
// Step 1: Check current permission state
const permissionResult = await checkPermission ();
// Step 2: If permission already granted, start immediately
if ( permissionResult . state === 'granted' || isPermissionGranted ) {
await startWalkthroughSession ();
return ;
}
// Step 3: Request permission if needed
if ( permissionResult . needsPermission ) {
await request ();
// The useEffect above will handle starting when permission is granted
}
} catch ( error ) {
console . error ( 'Error in handleStartWalkthrough:' , error );
setIsStarting ( false );
}
};
const handleRefresh = async () => {
await refresh ();
// Re-check after refresh
const result = await checkPermission ();
if ( result . state === 'granted' ) {
await startWalkthroughSession ();
}
};
const handleClose = () => {
setIsModalOpen ( false );
setIsStarting ( false );
};
if ( ! isModalOpen || ! guides ?. currentGuide ) return null ;
const renderModalContent = () => {
// Starting the walkthrough (permission already granted or being processed)
if ( isStarting && ( isPermissionGranted || state === 'granted' )) {
return (
< div className = "sammy-modal-auto-starting" >
< div className = "sammy-modal-spinner" />
< h3 > Starting Voice Assistant ...</ h3 >
< p > Preparing your guided experience </ p >
</ div >
);
}
// Permission denied - show instructions
if ( isPermissionDenied ) {
return (
< div className = "sammy-modal-permission-denied" >
< div className = "sammy-modal-icon" > 🚫 </ div >
< h3 > Microphone Access Blocked </ h3 >
< p > To use the voice - guided walkthrough , you need to grant microphone access : </ p >
< ol className = "sammy-modal-instructions" >
< li > Click the lock / info icon in your browser 's address bar</li >
< li > Find "Microphone" in the site settings </ li >
< li > Change from "Block" to "Allow" </ li >
< li > Click "Check Again" below </ li >
</ ol >
< div className = "sammy-modal-button-group" >
< button
className = "sammy-modal-btn sammy-modal-btn-secondary"
onClick = { handleClose }
>
Cancel
</ button >
< button
className = "sammy-modal-btn sammy-modal-btn-primary"
onClick = { handleRefresh }
>
🔄 Check Again
</ button >
</ div >
</ div >
);
}
// Error state
if ( state === 'error' && error ) {
return (
< div className = "sammy-modal-error" >
< div className = "sammy-modal-icon" > ⚠️ </ div >
< h3 > Microphone Error </ h3 >
< p className = "sammy-modal-error-message" > { error } </ p >
< div className = "sammy-modal-button-group" >
< button
className = "sammy-modal-btn sammy-modal-btn-secondary"
onClick = { handleClose }
>
Cancel
</ button >
< button
className = "sammy-modal-btn sammy-modal-btn-primary"
onClick = { handleRefresh }
>
Try Again
</ button >
</ div >
</ div >
);
}
// Browser not supported
if ( state === 'unsupported' ) {
return (
< div className = "sammy-modal-unsupported" >
< div className = "sammy-modal-icon" > ⚠️ </ div >
< h3 > Browser Not Supported </ h3 >
< p > Your browser doesn 't support microphone access. Please use Chrome, Firefox, or Safari.</p >
< div className = "sammy-modal-button-group" >
< button
className = "sammy-modal-btn sammy-modal-btn-primary"
onClick = { handleClose }
>
OK
</ button >
</ div >
</ div >
);
}
// Default: Initial prompt to start walkthrough
return (
< div className = "sammy-modal-initial" >
< div className = "sammy-modal-icon" >
< div className = "sammy-modal-speech-icon" > 💬 </ div >
</ div >
< h2 > Start AI Walkthrough Now ? </ h2 >
< p className = "sammy-modal-description" >
Our AI assistant will walk you through the platform in real time ,
answering your questions and guiding you step by step based on what 's
on your screen .
</ p >
< div className = "sammy-modal-features" >
< div className = "sammy-modal-feature" >
< span className = "sammy-modal-feature-icon" > 🎯 </ span >
< span > Real - time guidance through the platform </ span >
</ div >
< div className = "sammy-modal-feature" >
< span className = "sammy-modal-feature-icon" > 💬 </ span >
< span > Interactive Q & A as you navigate </ span >
</ div >
< div className = "sammy-modal-feature" >
< span className = "sammy-modal-feature-icon" > 🎤 </ span >
< span > Natural voice interaction </ span >
</ div >
</ div >
{needsPermission && (
<p className = "sammy-modal-permission-note" >
<span className = "sammy-modal-info-icon" > ℹ ️ </ span >
Clicking "Start Now" will request microphone access
</p>
)}
<div className= "sammy-modal-button-group" >
<button
className= "sammy-modal-btn sammy-modal-btn-secondary"
onClick={handleClose}
>
Not Now
</button>
<button
className= "sammy-modal-btn sammy-modal-btn-primary"
onClick={handleStartWalkthrough}
disabled={isStarting}
>
{isStarting ? 'Starting...' : 'Start Now' }
</ button >
</ div >
</ div >
);
};
return (
< div className = "sammy-modal-backdrop" onClick = { handleClose } >
< div className = "sammy-modal" onClick = {(e) => e.stopPropagation()} >
< button
className = "sammy-modal-close"
onClick = { handleClose }
aria - label = "Close modal"
>
×
</ button >
< div className = "sammy-modal-content" >
{ renderModalContent ()}
</ div >
</ div >
</ div >
);
};
See all 261 lines
Common Pitfalls and Mistakes
These are critical mistakes that will cause your microphone permission flow to fail. Review this section carefully to avoid common implementation errors.
❌ Incorrect Implementation (Will Fail)
Broken Implementation - DO NOT USE
const handleStartWalkthrough = async () => {
try {
// ❌ WRONG: Directly starting agent without permission check
handleCloseModal ();
setHasStartedWalkthrough ( true );
await startAgent ({
agentMode: AgentMode . USER ,
guideId: guides ?. currentGuide ?. guideId ,
});
} catch ( error ) {
console . error ( 'Error starting walkthrough:' , error );
}
};
✅ Correct Implementation
const handleStartWalkthrough = async () => {
setIsStarting ( true );
try {
// ✅ Step 1: ALWAYS check current permission state first
const permissionResult = await checkPermission ();
// ✅ Step 2: If permission already granted, start immediately
if ( permissionResult . state === 'granted' || isPermissionGranted ) {
await startWalkthroughSession ();
return ;
}
// ✅ Step 3: Request permission if needed
if ( permissionResult . needsPermission ) {
await request ();
// The useEffect will handle starting when permission is granted
}
} catch ( error ) {
console . error ( 'Error in handleStartWalkthrough:' , error );
setIsStarting ( false );
}
};
Critical Requirements Checklist
Missing Hook Usage
No Permission Check
Missing State Tracking
No Auto-Start Logic
Missing Error States
Problem Not using the useMicrophonePermission
hook at all // ❌ WRONG: No permission handling
const MyComponent = () => {
const { startAgent } = useSammyAgentContext ();
const handleStart = async () => {
await startAgent ({ agentMode: AgentMode . USER }); // Will fail!
};
};
Solution Always import and use the permission hook // ✅ CORRECT: Using permission hook
const MyComponent = () => {
const { startAgent } = useSammyAgentContext ();
const {
state ,
error ,
isPermissionGranted ,
isPermissionDenied ,
needsPermission ,
checkPermission ,
request ,
refresh
} = useMicrophonePermission (); // Essential!
const handleStart = async () => {
const result = await checkPermission ();
if ( result . needsPermission ) {
await request ();
}
if ( isPermissionGranted ) {
await startAgent ({ agentMode: AgentMode . USER });
}
};
};
Key Implementation Points
Essential Implementation Pattern The correct pattern ALWAYS includes these elements:
Import and use useMicrophonePermission
hook
Check permission state before starting agent
Handle permission request if needed
Track loading/starting state
Implement auto-start logic with useEffect
Handle all permission states in UI
Provide recovery options for denied/error states
Troubleshooting
Common Issues
Permission State Not Updating
Use the refresh()
method to force a re-check of the permission state: const handlePermissionChange = async () => {
await refresh ();
const newState = await checkPermission ();
console . log ( 'Updated state:' , newState );
};
Browser Compatibility Issues
Check for browser support before attempting to use microphone features: const checkBrowserSupport = () => {
if ( ! navigator . mediaDevices || ! navigator . mediaDevices . getUserMedia ) {
return {
supported: false ,
message: 'Your browser doesn \' t support microphone access. Please use Chrome, Firefox, or Safari.'
};
}
// Check for HTTPS (required for getUserMedia)
if ( location . protocol !== 'https:' && location . hostname !== 'localhost' ) {
return {
supported: false ,
message: 'Microphone access requires HTTPS. Please use a secure connection.'
};
}
return { supported: true };
};
Handling Permission Reset
Re-check permissions when the browser tab becomes visible: useEffect (() => {
const handleVisibilityChange = () => {
if ( document . visibilityState === 'visible' ) {
// Re-check permissions when tab becomes visible
checkPermission ();
}
};
document . addEventListener ( 'visibilitychange' , handleVisibilityChange );
return () => {
document . removeEventListener ( 'visibilitychange' , handleVisibilityChange );
};
}, [ checkPermission ]);
Stream Cleanup
Always ensure proper cleanup of media streams: useEffect (() => {
return () => {
// Hook handles this automatically, but for custom implementations:
if ( stream ) {
stream . getTracks (). forEach ( track => track . stop ());
}
};
}, [ stream ]);
Best Practices
Best Practices Checklist Follow these guidelines for optimal microphone permission handling:
Always explain why you need microphone access before requesting
Check permission state before attempting to request
Handle all states explicitly with appropriate UI
Provide clear instructions for permission recovery
Test across browsers and handle compatibility issues
Clean up resources properly when component unmounts
Use polling sparingly - only when expecting user action
Provide fallbacks for when voice isn’t available
Log errors for debugging but show user-friendly messages
Respect user choice - don’t repeatedly prompt if denied
Remember that microphone permission is a sensitive user action - always be transparent about why you need it and what you’ll use it for. The modal-based pattern with clear messaging and progressive disclosure provides the best user experience for most use cases.
Next Steps