'use client'; import { useMemo } from 'react'; import { useBrowserDetect, useDeviceDetect } from '@djangocfg/ui-core/hooks'; import type { RecognitionEngine } from '../types'; import { createWebSpeechEngine } from '../core/engine/webspeech'; export type VoiceUnsupportedReason = | 'no-engine' | 'no-mediadevices' | 'in-app-browser' | 'unknown'; export interface VoiceSupport { /** Should the host render the voice button at all? */ supported: boolean; /** Why is it off? `null` when supported. */ reason: VoiceUnsupportedReason | null; /** Engine id that would be used if rendered (for badges / telemetry). */ engineId: string | null; /** True on a mobile UA — useful for hiding on phones if the host wants. */ isMobile: boolean; /** True inside Facebook / Instagram / TikTok / etc. in-app browsers. */ isInApp: boolean; } /** * Decides whether the `` should render the mic * button. Three gates: * * 1. A `RecognitionEngine` reports `isSupported === true`. With no * custom engine passed we probe `createWebSpeechEngine()` — * Chrome / Edge / Safari pass, Firefox does not. * 2. `navigator.mediaDevices.getUserMedia` exists. Required even when * using Web Speech (browser still asks the user for mic access). * 3. The host browser is not a known in-app WebView (Instagram, * Facebook, TikTok, …) — mic permission flows are broken in most * of them. The host can override this gate by passing a custom * engine that already handles the case. */ export function useVoiceSupport(engine?: RecognitionEngine): VoiceSupport { const browser = useBrowserDetect(); const device = useDeviceDetect(); return useMemo(() => { const isMobile = device.selectors.isMobile; const isInApp = browser.isInAppBrowser; const hasMediaDevices = typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia; if (!hasMediaDevices) { return { supported: false, reason: 'no-mediadevices', engineId: null, isMobile, isInApp }; } // If the host didn't pass a custom engine we can only fall back to // the browser Web Speech API — bail when it's not available // (Firefox / some WebViews). const probe = engine ?? createWebSpeechEngine(); if (!probe.isSupported) { // In-app browsers are the most common reason a probe fails on // mobile; surface a friendlier code so the UI can hide silently. if (isInApp) { return { supported: false, reason: 'in-app-browser', engineId: null, isMobile, isInApp }; } return { supported: false, reason: 'no-engine', engineId: null, isMobile, isInApp }; } return { supported: true, reason: null, engineId: probe.id, isMobile, isInApp, }; }, [engine, browser.isInAppBrowser, device.selectors.isMobile]); }