usePermission
A React hook for managing browser permissions (camera, microphone, notifications, geolocation, etc.) with unified interface for checking and requesting permissions.
The usePermission hook provides a comprehensive solution for managing browser permissions in React applications. It offers a unified interface for checking and requesting various browser permissions like camera, microphone, notifications, geolocation, and more, with automatic status monitoring and error handling.
Basic Usage
Single Permission
import { usePermission } from "light-hooks";
function CameraAccess() {
  const { permissionStatus, requestPermissions, isLoading } = usePermission('camera');
  const cameraPermission = permissionStatus[0];
  return (
    <div>
      <h2>Camera Access</h2>
      <p>Status: {cameraPermission?.state || 'Unknown'}</p>
      
      {cameraPermission?.state === 'granted' && (
        <p>β
 Camera access granted!</p>
      )}
      
      {cameraPermission?.state === 'denied' && (
        <p>β Camera access denied</p>
      )}
      
      {cameraPermission?.state === 'prompt' && (
        <button onClick={requestPermissions} disabled={isLoading}>
          {isLoading ? 'Requesting...' : 'Request Camera Access'}
        </button>
      )}
    </div>
  );
}
Multiple Permissions
function MediaAccess() {
  const { permissionStatus, requestPermissions, isLoading, error } = usePermission([
    'camera',
    'microphone'
  ]);
  const [cameraStatus, microphoneStatus] = permissionStatus;
  return (
    <div>
      <h2>Media Permissions</h2>
      
      <div>
        <h3>Camera: {cameraStatus?.state || 'Unknown'}</h3>
        <h3>Microphone: {microphoneStatus?.state || 'Unknown'}</h3>
      </div>
      {error && (
        <p style={{ color: 'red' }}>Error: {error}</p>
      )}
      <button onClick={requestPermissions} disabled={isLoading}>
        {isLoading ? 'Requesting...' : 'Request Media Access'}
      </button>
    </div>
  );
}
API Reference
Parameters
The hook accepts different types of permission inputs:
| Type | Description | Example | 
|---|---|---|
PermissionName | Single permission name | usePermission('camera') | 
PermissionDescriptor | Permission with additional options | usePermission({ name: 'midi', sysex: true }) | 
Array | Multiple permissions | usePermission(['camera', 'microphone']) | 
usePermissionOptions Type
type usePermissionOptions = 
  | PermissionName           // Single permission name
  | PermissionDescriptor     // Permission with options
  | PermissionType[];        // Array of permissions
Return Value
Returns a usePermissionResult object with:
| Property | Type | Description | 
|---|---|---|
permissionStatus | PermissionStatus[] | Array of current permission statuses | 
requestPermissions | () => Promise<void> | Function to request permissions from user | 
checkPermissions | () => Promise<void> | Function to check status without requesting | 
isLoading | boolean | Whether permission operations are in progress | 
error | string | null | Error message if operations failed | 
Permission States
Each PermissionStatus has a state property with possible values:
| State | Description | 
|---|---|
'granted' | User has granted permission | 
'denied' | User has denied permission | 
'prompt' | Browser will prompt user when permission is needed | 
Supported Permissions
| Permission | Description | Additional Options | 
|---|---|---|
'camera' | Camera access for video capture | - | 
'microphone' | Microphone access for audio capture | - | 
'notifications' | Show browser notifications | - | 
'geolocation' | Access device location | - | 
'midi' | MIDI device access | sysex: boolean | 
'persistent-storage' | Persistent storage quota | - | 
'push' | Push notifications (requires service worker) | - | 
Examples
Notification Permission Manager
function NotificationManager() {
  const { permissionStatus, requestPermissions, isLoading } = usePermission('notifications');
  
  const notificationPermission = permissionStatus[0];
  const [message, setMessage] = useState('');
  const sendNotification = () => {
    if (notificationPermission?.state === 'granted') {
      new Notification('Test Notification', {
        body: message || 'Hello from your app!',
        icon: '/icon.png'
      });
    }
  };
  return (
    <div className="notification-manager">
      <h2>Notification Manager</h2>
      
      <div className="status">
        <p>Permission Status: <span className={notificationPermission?.state}>
          {notificationPermission?.state || 'Unknown'}
        </span></p>
      </div>
      {notificationPermission?.state === 'prompt' && (
        <button onClick={requestPermissions} disabled={isLoading}>
          {isLoading ? 'Requesting...' : 'Enable Notifications'}
        </button>
      )}
      {notificationPermission?.state === 'granted' && (
        <div className="notification-controls">
          <input
            type="text"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            placeholder="Enter notification message"
          />
          <button onClick={sendNotification}>
            Send Test Notification
          </button>
        </div>
      )}
      {notificationPermission?.state === 'denied' && (
        <div className="denied-help">
          <p>β Notifications are blocked</p>
          <p>To enable notifications:</p>
          <ol>
            <li>Click the lock icon in your browser's address bar</li>
            <li>Change notifications to "Allow"</li>
            <li>Refresh the page</li>
          </ol>
        </div>
      )}
    </div>
  );
}
Location Permission with Map
function LocationTracker() {
  const { permissionStatus, requestPermissions, isLoading, error } = usePermission('geolocation');
  const [position, setPosition] = useState<GeolocationPosition | null>(null);
  const [locationError, setLocationError] = useState<string | null>(null);
  const locationPermission = permissionStatus[0];
  const getCurrentLocation = async () => {
    if (locationPermission?.state !== 'granted') {
      await requestPermissions();
      return;
    }
    navigator.geolocation.getCurrentPosition(
      (pos) => {
        setPosition(pos);
        setLocationError(null);
      },
      (err) => {
        setLocationError(err.message);
      },
      { enableHighAccuracy: true }
    );
  };
  return (
    <div className="location-tracker">
      <h2>Location Tracker</h2>
      
      <div className="permission-status">
        <p>Location Permission: {locationPermission?.state || 'Unknown'}</p>
        {error && <p className="error">Permission Error: {error}</p>}
        {locationError && <p className="error">Location Error: {locationError}</p>}
      </div>
      <button onClick={getCurrentLocation} disabled={isLoading}>
        {isLoading ? 'Getting Location...' : 'Get My Location'}
      </button>
      {position && (
        <div className="location-info">
          <h3>Current Location</h3>
          <p>Latitude: {position.coords.latitude.toFixed(6)}</p>
          <p>Longitude: {position.coords.longitude.toFixed(6)}</p>
          <p>Accuracy: {position.coords.accuracy.toFixed(0)} meters</p>
          <p>Timestamp: {new Date(position.timestamp).toLocaleString()}</p>
        </div>
      )}
    </div>
  );
}
Media Device Manager
function MediaDeviceManager() {
  const { permissionStatus, requestPermissions, isLoading } = usePermission([
    'camera',
    'microphone'
  ]);
  const [cameraPermission, microphonePermission] = permissionStatus;
  const [stream, setStream] = useState<MediaStream | null>(null);
  const videoRef = useRef<HTMLVideoElement>(null);
  const startCamera = async () => {
    try {
      if (cameraPermission?.state !== 'granted') {
        await requestPermissions();
        return;
      }
      const mediaStream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: microphonePermission?.state === 'granted'
      });
      setStream(mediaStream);
      if (videoRef.current) {
        videoRef.current.srcObject = mediaStream;
      }
    } catch (error) {
      console.error('Failed to start camera:', error);
    }
  };
  const stopCamera = () => {
    if (stream) {
      stream.getTracks().forEach(track => track.stop());
      setStream(null);
    }
  };
  return (
    <div className="media-manager">
      <h2>Camera & Microphone Manager</h2>
      
      <div className="permissions-status">
        <div className="permission-item">
          <span>Camera: </span>
          <span className={`status ${cameraPermission?.state}`}>
            {cameraPermission?.state || 'Unknown'}
          </span>
        </div>
        <div className="permission-item">
          <span>Microphone: </span>
          <span className={`status ${microphonePermission?.state}`}>
            {microphonePermission?.state || 'Unknown'}
          </span>
        </div>
      </div>
      <div className="controls">
        {!stream ? (
          <button onClick={startCamera} disabled={isLoading}>
            {isLoading ? 'Starting...' : 'Start Camera'}
          </button>
        ) : (
          <button onClick={stopCamera}>Stop Camera</button>
        )}
        
        <button onClick={requestPermissions} disabled={isLoading}>
          {isLoading ? 'Requesting...' : 'Request All Permissions'}
        </button>
      </div>
      {stream && (
        <div className="video-container">
          <video
            ref={videoRef}
            autoPlay
            playsInline
            muted
            style={{ width: '100%', maxWidth: '500px', borderRadius: '8px' }}
          />
        </div>
      )}
    </div>
  );
}
MIDI Device Access
function MIDIController() {
  const { permissionStatus, requestPermissions, isLoading } = usePermission({
    name: 'midi',
    sysex: true
  });
  const [midiAccess, setMidiAccess] = useState<WebMidi.MIDIAccess | null>(null);
  const [connectedDevices, setConnectedDevices] = useState<string[]>([]);
  const midiPermission = permissionStatus[0];
  const connectMIDI = async () => {
    try {
      if (midiPermission?.state !== 'granted') {
        await requestPermissions();
        return;
      }
      const access = await navigator.requestMIDIAccess({ sysex: true });
      setMidiAccess(access);
      // List connected devices
      const devices: string[] = [];
      access.inputs.forEach((input) => {
        devices.push(`Input: ${input.name}`);
      });
      access.outputs.forEach((output) => {
        devices.push(`Output: ${output.name}`);
      });
      setConnectedDevices(devices);
    } catch (error) {
      console.error('Failed to access MIDI devices:', error);
    }
  };
  return (
    <div className="midi-controller">
      <h2>MIDI Device Controller</h2>
      
      <p>MIDI Permission: {midiPermission?.state || 'Unknown'}</p>
      <button onClick={connectMIDI} disabled={isLoading}>
        {isLoading ? 'Connecting...' : 'Connect MIDI Devices'}
      </button>
      {midiAccess && (
        <div className="device-list">
          <h3>Connected MIDI Devices</h3>
          {connectedDevices.length > 0 ? (
            <ul>
              {connectedDevices.map((device, index) => (
                <li key={index}>{device}</li>
              ))}
            </ul>
          ) : (
            <p>No MIDI devices found</p>
          )}
        </div>
      )}
    </div>
  );
}
Permission Status Dashboard
function PermissionDashboard() {
  const { permissionStatus, requestPermissions, checkPermissions, isLoading } = usePermission([
    'camera',
    'microphone',
    'notifications',
    'geolocation',
    'persistent-storage'
  ]);
  const permissionNames = ['camera', 'microphone', 'notifications', 'geolocation', 'persistent-storage'];
  const getStatusIcon = (state: PermissionState | undefined) => {
    switch (state) {
      case 'granted': return 'β
';
      case 'denied': return 'β';
      case 'prompt': return 'β';
      default: return 'β³';
    }
  };
  const getStatusColor = (state: PermissionState | undefined) => {
    switch (state) {
      case 'granted': return '#4caf50';
      case 'denied': return '#f44336';
      case 'prompt': return '#ff9800';
      default: return '#9e9e9e';
    }
  };
  return (
    <div className="permission-dashboard">
      <h2>Permission Status Dashboard</h2>
      
      <div className="actions">
        <button onClick={checkPermissions} disabled={isLoading}>
          {isLoading ? 'Checking...' : 'Refresh Status'}
        </button>
        <button onClick={requestPermissions} disabled={isLoading}>
          {isLoading ? 'Requesting...' : 'Request All Permissions'}
        </button>
      </div>
      <div className="permissions-grid">
        {permissionNames.map((name, index) => {
          const status = permissionStatus[index];
          return (
            <div 
              key={name} 
              className="permission-card"
              style={{ borderColor: getStatusColor(status?.state) }}
            >
              <div className="permission-header">
                <span className="icon">{getStatusIcon(status?.state)}</span>
                <h3>{name}</h3>
              </div>
              <p className="status" style={{ color: getStatusColor(status?.state) }}>
                {status?.state || 'Unknown'}
              </p>
            </div>
          );
        })}
      </div>
      <div className="legend">
        <h3>Permission States</h3>
        <div className="legend-items">
          <span>β
 Granted - Permission is allowed</span>
          <span>β Denied - Permission is blocked</span>
          <span>β Prompt - Will ask when needed</span>
          <span>β³ Unknown - Status not determined</span>
        </div>
      </div>
    </div>
  );
}
Persistent Storage Manager
function StorageManager() {
  const { permissionStatus, requestPermissions, isLoading } = usePermission('persistent-storage');
  const [storageInfo, setStorageInfo] = useState<{
    quota: number;
    usage: number;
    persistent: boolean;
  } | null>(null);
  const storagePermission = permissionStatus[0];
  const checkStorageInfo = async () => {
    if ('storage' in navigator && 'estimate' in navigator.storage) {
      const estimate = await navigator.storage.estimate();
      const persistent = await navigator.storage.persisted();
      
      setStorageInfo({
        quota: estimate.quota || 0,
        usage: estimate.usage || 0,
        persistent
      });
    }
  };
  const requestPersistentStorage = async () => {
    await requestPermissions();
    await checkStorageInfo();
  };
  useEffect(() => {
    checkStorageInfo();
  }, []);
  const formatBytes = (bytes: number) => {
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    if (bytes === 0) return '0 Bytes';
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
  };
  return (
    <div className="storage-manager">
      <h2>Persistent Storage Manager</h2>
      
      <div className="permission-status">
        <p>Storage Permission: {storagePermission?.state || 'Unknown'}</p>
        <p>Persistent Storage: {storageInfo?.persistent ? 'Enabled' : 'Disabled'}</p>
      </div>
      {storageInfo && (
        <div className="storage-info">
          <h3>Storage Information</h3>
          <p>Used: {formatBytes(storageInfo.usage)}</p>
          <p>Available: {formatBytes(storageInfo.quota)}</p>
          <div className="usage-bar">
            <div 
              className="usage-fill"
              style={{
                width: `${(storageInfo.usage / storageInfo.quota) * 100}%`,
                backgroundColor: storageInfo.persistent ? '#4caf50' : '#ff9800'
              }}
            />
          </div>
          <p className="usage-percent">
            {((storageInfo.usage / storageInfo.quota) * 100).toFixed(1)}% used
          </p>
        </div>
      )}
      <div className="actions">
        <button onClick={requestPersistentStorage} disabled={isLoading}>
          {isLoading ? 'Requesting...' : 'Request Persistent Storage'}
        </button>
        <button onClick={checkStorageInfo}>
          Refresh Storage Info
        </button>
      </div>
      <div className="storage-explanation">
        <h3>About Persistent Storage</h3>
        <p>
          Persistent storage prevents your data from being automatically cleared
          by the browser when storage space is low. This is useful for offline
          applications and storing important user data.
        </p>
      </div>
    </div>
  );
}
Best Practices
1. Check Before Requesting
// β
 Good: Check status before requesting
const { permissionStatus, requestPermissions } = usePermission('camera');
const cameraPermission = permissionStatus[0];
const handleCameraAccess = () => {
  if (cameraPermission?.state === 'granted') {
    // Use camera directly
    startCamera();
  } else if (cameraPermission?.state === 'prompt') {
    // Request permission first
    requestPermissions().then(() => {
      if (permissionStatus[0]?.state === 'granted') {
        startCamera();
      }
    });
  } else {
    // Handle denied state
    showPermissionDeniedMessage();
  }
};
// β Avoid: Requesting without checking
const bad = () => {
  requestPermissions(); // May show unnecessary prompts
};
2. Handle Permission States Appropriately
// β
 Good: Provide guidance for each state
function PermissionHandler() {
  const { permissionStatus, requestPermissions } = usePermission('notifications');
  const permission = permissionStatus[0];
  switch (permission?.state) {
    case 'granted':
      return <div>β
 Notifications enabled!</div>;
    
    case 'denied':
      return (
        <div>
          β Notifications blocked. 
          <a href="/help/enable-notifications">Learn how to enable</a>
        </div>
      );
    
    case 'prompt':
      return (
        <button onClick={requestPermissions}>
          Enable Notifications
        </button>
      );
    
    default:
      return <div>β³ Checking permission status...</div>;
  }
}
3. Group Related Permissions
// β
 Good: Request related permissions together
const mediaPermissions = usePermission(['camera', 'microphone']);
// β
 Good: Separate unrelated permissions
const cameraPermission = usePermission('camera');
const notificationPermission = usePermission('notifications');
// β Avoid: Mixing unrelated permissions unnecessarily
const mixed = usePermission(['camera', 'notifications', 'geolocation']); // Too broad
4. Provide Fallback Options
// β
 Good: Offer alternatives when permission is denied
function MediaCapture() {
  const { permissionStatus, requestPermissions } = usePermission('camera');
  const cameraPermission = permissionStatus[0];
  if (cameraPermission?.state === 'denied') {
    return (
      <div>
        <p>Camera access is blocked</p>
        <button onClick={() => document.getElementById('file-input')?.click()}>
          Upload Photo Instead
        </button>
        <input
          id="file-input"
          type="file"
          accept="image/*"
          style={{ display: 'none' }}
          onChange={handleFileUpload}
        />
      </div>
    );
  }
  return (
    <button onClick={requestPermissions}>
      Enable Camera
    </button>
  );
}
5. Monitor Permission Changes
// β
 Good: The hook automatically monitors changes
function PermissionMonitor() {
  const { permissionStatus } = usePermission('notifications');
  const permission = permissionStatus[0];
  useEffect(() => {
    // Automatically called when permission state changes
    console.log('Permission state changed:', permission?.state);
  }, [permission?.state]);
  return <div>Status: {permission?.state}</div>;
}
6. Handle Errors Gracefully
// β
 Good: Handle permission errors
function RobustPermissionHandler() {
  const { permissionStatus, requestPermissions, error } = usePermission('camera');
  if (error) {
    return (
      <div className="error">
        <p>Permission error: {error}</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }
  return (
    <button onClick={requestPermissions}>
      Request Camera Access
    </button>
  );
}
TypeScript
The hook is fully typed with comprehensive interfaces:
import { usePermission, usePermissionResult, usePermissionOptions } from "light-hooks";
// Type inference works automatically
const result = usePermission('camera');
// result: usePermissionResult
// Explicit typing (optional)
const options: usePermissionOptions = ['camera', 'microphone'];
const explicitResult: usePermissionResult = usePermission(options);
// Custom component with typed props
interface PermissionGateProps {
  permission: PermissionName;
  onGranted: () => void;
  onDenied: () => void;
  children: React.ReactNode;
}
function PermissionGate({ 
  permission, 
  onGranted, 
  onDenied, 
  children 
}: PermissionGateProps) {
  const { permissionStatus, requestPermissions } = usePermission(permission);
  const status = permissionStatus[0];
  useEffect(() => {
    if (status?.state === 'granted') {
      onGranted();
    } else if (status?.state === 'denied') {
      onDenied();
    }
  }, [status?.state, onGranted, onDenied]);
  if (status?.state === 'granted') {
    return <>{children}</>;
  }
  return (
    <button onClick={requestPermissions}>
      Grant {permission} permission
    </button>
  );
}
Interface Definitions
interface usePermissionResult {
  permissionStatus: PermissionStatus[];
  requestPermissions: () => Promise<void>;
  checkPermissions: () => Promise<void>;
  isLoading: boolean;
  error: string | null;
}
type usePermissionOptions = 
  | PermissionName 
  | PermissionDescriptor 
  | PermissionType[];
// Browser-native interfaces
interface PermissionStatus {
  state: PermissionState; // 'granted' | 'denied' | 'prompt'
  name: string;
  addEventListener(type: 'change', listener: () => void): void;
  removeEventListener(type: 'change', listener: () => void): void;
}
Common Issues
Permission Request Timing
// β Problem: Requesting permissions on page load
useEffect(() => {
  requestPermissions(); // Unexpected prompts
}, []);
// β
 Solution: Request on user interaction
<button onClick={requestPermissions}>
  Enable Camera
</button>
Browser Compatibility
// β
 Good: Check for API support
function PermissionWrapper() {
  if (!('permissions' in navigator)) {
    return <div>Permissions API not supported</div>;
  }
  return <PermissionComponent />;
}
// β
 Good: Handle unsupported permissions gracefully
const { permissionStatus, error } = usePermission('midi');
if (error?.includes('not supported')) {
  return <div>MIDI not supported in this browser</div>;
}
Service Worker Requirements
// β
 Good: Check for service worker before requesting push
function PushNotifications() {
  const { permissionStatus, requestPermissions } = usePermission('push');
  const handleRequest = async () => {
    if (!('serviceWorker' in navigator)) {
      alert('Service workers not supported');
      return;
    }
    try {
      await navigator.serviceWorker.register('/sw.js');
      await requestPermissions();
    } catch (error) {
      console.error('Failed to register service worker:', error);
    }
  };
  return <button onClick={handleRequest}>Enable Push Notifications</button>;
}
Memory Leaks Prevention
// β
 Good: Hook automatically handles cleanup
function Component() {
  const { permissionStatus } = usePermission('camera');
  // Event listeners are automatically cleaned up
  return <div>{permissionStatus[0]?.state}</div>;
}
// β Avoid: Manual event listeners without cleanup
useEffect(() => {
  navigator.permissions.query({ name: 'camera' }).then(status => {
    status.addEventListener('change', handler); // Need manual cleanup
  });
}, []);
Advanced Usage
Permission State Machine
function PermissionStateMachine({ permission }: { permission: PermissionName }) {
  const { permissionStatus, requestPermissions, isLoading } = usePermission(permission);
  const [userAction, setUserAction] = useState<'none' | 'requesting' | 'completed'>('none');
  
  const status = permissionStatus[0];
  const handleRequest = async () => {
    setUserAction('requesting');
    await requestPermissions();
    setUserAction('completed');
  };
  // State-based rendering
  const getContent = () => {
    if (isLoading) return <div>β³ Loading...</div>;
    
    switch (status?.state) {
      case 'granted':
        return <div>β
 {permission} access granted</div>;
        
      case 'denied':
        if (userAction === 'completed') {
          return (
            <div>
              β Permission denied. Please enable in browser settings.
              <button onClick={() => window.location.reload()}>
                Retry
              </button>
            </div>
          );
        }
        return <div>β {permission} access was previously denied</div>;
        
      case 'prompt':
        return (
          <button onClick={handleRequest} disabled={userAction === 'requesting'}>
            {userAction === 'requesting' ? 'Requesting...' : `Enable ${permission}`}
          </button>
        );
        
      default:
        return <div>β³ Checking {permission} permission...</div>;
    }
  };
  return <div className="permission-state-machine">{getContent()}</div>;
}
Conditional Permission Loading
function ConditionalPermissions({ features }: { features: string[] }) {
  const permissions = useMemo(() => {
    const perms: PermissionName[] = [];
    
    if (features.includes('video-call')) {
      perms.push('camera', 'microphone');
    }
    if (features.includes('notifications')) {
      perms.push('notifications');
    }
    if (features.includes('location')) {
      perms.push('geolocation');
    }
    
    return perms;
  }, [features]);
  const { permissionStatus, requestPermissions } = usePermission(permissions);
  return (
    <div>
      <h3>Required Permissions for {features.join(', ')}</h3>
      {permissions.map((permission, index) => (
        <div key={permission}>
          {permission}: {permissionStatus[index]?.state || 'Unknown'}
        </div>
      ))}
      <button onClick={requestPermissions}>
        Request All Permissions
      </button>
    </div>
  );
}
Permission-Based Feature Gates
function FeatureGate({ 
  requiredPermissions, 
  children,
  fallback 
}: {
  requiredPermissions: PermissionName[];
  children: React.ReactNode;
  fallback?: React.ReactNode;
}) {
  const { permissionStatus, requestPermissions } = usePermission(requiredPermissions);
  
  const allGranted = permissionStatus.every(status => status?.state === 'granted');
  const anyDenied = permissionStatus.some(status => status?.state === 'denied');
  if (allGranted) {
    return <>{children}</>;
  }
  if (anyDenied) {
    return fallback || <div>Some required permissions are denied</div>;
  }
  return (
    <div className="permission-gate">
      <p>This feature requires the following permissions:</p>
      <ul>
        {requiredPermissions.map((permission, index) => (
          <li key={permission}>
            {permission}: {permissionStatus[index]?.state || 'Unknown'}
          </li>
        ))}
      </ul>
      <button onClick={requestPermissions}>
        Grant Permissions
      </button>
    </div>
  );
}
// Usage
function VideoCallApp() {
  return (
    <FeatureGate 
      requiredPermissions={['camera', 'microphone']}
      fallback={<div>Video calling requires camera and microphone access</div>}
    >
      <VideoCallInterface />
    </FeatureGate>
  );
}