From f2803ca5db759664ebcb825bd686be7b4600aae9 Mon Sep 17 00:00:00 2001 From: Sergei Date: Tue, 27 Jan 2026 17:03:56 -0800 Subject: [PATCH] fix(stt): graceful degradation for Expo Go Handle missing native module @jamsch/expo-speech-recognition gracefully. In Expo Go the native module is not available, which was causing the entire _layout.tsx to fail to export, breaking tab navigation. - Use dynamic require() with try/catch instead of static import - Initialize ExpoSpeechRecognitionModule and useSpeechRecognitionEvent as no-ops - Check module availability before calling any native methods - isAvailable state properly reflects module presence Tab navigation now works in Expo Go (with STT disabled). Full STT functionality requires a development build. --- hooks/useSpeechRecognition.ts | 40 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/hooks/useSpeechRecognition.ts b/hooks/useSpeechRecognition.ts index a84501f..c14f985 100644 --- a/hooks/useSpeechRecognition.ts +++ b/hooks/useSpeechRecognition.ts @@ -4,6 +4,10 @@ * Wraps @jamsch/expo-speech-recognition for easy use in components. * Provides start/stop controls, recognized text, and status states. * + * NOTE: Gracefully handles missing native module (Expo Go) + * - In Expo Go: isAvailable = false, all methods are no-ops + * - In Dev Build: Full functionality + * * Usage: * ```typescript * const { startListening, stopListening, isListening, recognizedText, error } = useSpeechRecognition(); @@ -19,12 +23,20 @@ */ import { useState, useCallback, useRef, useEffect } from 'react'; -import { - ExpoSpeechRecognitionModule, - useSpeechRecognitionEvent, -} from '@jamsch/expo-speech-recognition'; import { Platform } from 'react-native'; +// Try to import the native module - may fail in Expo Go +let ExpoSpeechRecognitionModule: any = null; +let useSpeechRecognitionEvent: any = () => {}; // no-op by default + +try { + const speechRecognition = require('@jamsch/expo-speech-recognition'); + ExpoSpeechRecognitionModule = speechRecognition.ExpoSpeechRecognitionModule; + useSpeechRecognitionEvent = speechRecognition.useSpeechRecognitionEvent; +} catch (e) { + console.warn('[SpeechRecognition] Native module not available (Expo Go?). Speech recognition disabled.'); +} + export interface UseSpeechRecognitionOptions { /** Language for recognition (default: 'en-US') */ lang?: string; @@ -83,7 +95,7 @@ export function useSpeechRecognition( } = options; const [isListening, setIsListening] = useState(false); - const [isAvailable, setIsAvailable] = useState(true); + const [isAvailable, setIsAvailable] = useState(!!ExpoSpeechRecognitionModule); const [recognizedText, setRecognizedText] = useState(''); const [partialTranscript, setPartialTranscript] = useState(''); const [error, setError] = useState(null); @@ -95,6 +107,11 @@ export function useSpeechRecognition( // Check availability on mount useEffect(() => { + if (!ExpoSpeechRecognitionModule) { + setIsAvailable(false); + return; + } + const checkAvailability = async () => { try { // Check if we can get permissions (indirect availability check) @@ -131,7 +148,7 @@ export function useSpeechRecognition( }); // Event: Result available - useSpeechRecognitionEvent('result', (event) => { + useSpeechRecognitionEvent('result', (event: any) => { const results = event.results; if (results && results.length > 0) { const result = results[results.length - 1]; @@ -159,7 +176,7 @@ export function useSpeechRecognition( }); // Event: Error occurred - useSpeechRecognitionEvent('error', (event) => { + useSpeechRecognitionEvent('error', (event: any) => { const errorMessage = event.message || event.error || 'Speech recognition error'; console.error('[SpeechRecognition] Error:', errorMessage); @@ -178,6 +195,11 @@ export function useSpeechRecognition( * @returns true if started successfully, false otherwise */ const startListening = useCallback(async (): Promise => { + if (!ExpoSpeechRecognitionModule) { + console.warn('[SpeechRecognition] Cannot start - native module not available'); + return false; + } + if (isListening || isStartingRef.current) { console.log('[SpeechRecognition] Already listening or starting'); return false; @@ -239,6 +261,8 @@ export function useSpeechRecognition( * Stop listening and process final result */ const stopListening = useCallback(() => { + if (!ExpoSpeechRecognitionModule) return; + if (!isListening && !isStartingRef.current) { console.log('[SpeechRecognition] Not listening, nothing to stop'); return; @@ -256,6 +280,8 @@ export function useSpeechRecognition( * Abort listening without processing */ const abortListening = useCallback(() => { + if (!ExpoSpeechRecognitionModule) return; + if (!isListening && !isStartingRef.current) { return; }