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.
This commit is contained in:
Sergei 2026-01-27 17:03:56 -08:00
parent 76d93abf1e
commit f2803ca5db

View File

@ -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<string | null>(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<boolean> => {
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;
}