Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions __mocks__/@livekit/react-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Mock for @livekit/react-native
export const AudioSession = {
startAudioSession: jest.fn().mockResolvedValue(undefined),
stopAudioSession: jest.fn().mockResolvedValue(undefined),
configureAudio: jest.fn().mockResolvedValue(undefined),
getAudioOutputs: jest.fn().mockResolvedValue([]),
selectAudioOutput: jest.fn().mockResolvedValue(undefined),
showAudioRoutePicker: jest.fn().mockResolvedValue(undefined),
setAppleAudioConfiguration: jest.fn().mockResolvedValue(undefined),
};

export const registerGlobals = jest.fn();

export default {
AudioSession,
registerGlobals,
};
82 changes: 79 additions & 3 deletions src/stores/app/livekit-store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AudioSession } from '@livekit/react-native';
import { RTCAudioSession } from '@livekit/react-native-webrtc';
import notifee, { AndroidForegroundServiceType, AndroidImportance } from '@notifee/react-native';
import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from 'expo-audio';
Expand Down Expand Up @@ -381,18 +382,58 @@ export const useLiveKitStore = create<LiveKitState>((set, get) => ({
message: 'Cannot connect to room - permissions not granted',
context: { roomName: roomInfo.Name },
});
Alert.alert('Voice Connection Error', 'Microphone permission is required to join a voice channel. Please grant the permission in your device settings.', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
]);
return;
}

const { currentRoom, voipServerWebsocketSslAddress } = get();

// Disconnect from current room if connected
// Validate connection parameters before attempting to connect
if (!voipServerWebsocketSslAddress) {
logger.error({
message: 'Cannot connect to room - no VoIP server address available',
context: { roomName: roomInfo.Name },
});
Alert.alert('Voice Connection Error', 'Voice server address is not available. Please try again later.');
return;
}

if (!token) {
logger.error({
message: 'Cannot connect to room - no token provided',
context: { roomName: roomInfo.Name },
});
Alert.alert('Voice Connection Error', 'Voice channel token is missing. Please try refreshing the voice channels.');
return;
}
Comment on lines +385 to +411
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize new user-facing Alert strings.

These new alerts introduce hardcoded English strings; route them through t() with translation keys so they’re localized.

💡 Example adjustment
- Alert.alert('Voice Connection Error', 'Microphone permission is required to join a voice channel. Please grant the permission in your device settings.', [
+ Alert.alert(t('voice.connectionErrorTitle'), t('voice.micPermissionRequired'), [
    { text: t('common.cancel'), style: 'cancel' },
    { text: t('common.openSettings'), onPress: () => Linking.openSettings() },
  ]);

As per coding guidelines, "Ensure all text is wrapped in t() from react-i18next for translations with dictionary files stored in src/translations".

Also applies to: 638-642

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/app/livekit-store.ts` around lines 385 - 411, Replace hardcoded
Alert.alert messages with localized strings by calling t() from react-i18next:
wrap each user-facing title and body string in t() using descriptive translation
keys (e.g., 'voice.connectionError', 'voice.serverAddressUnavailable',
'voice.tokenMissing', 'voice.microphonePermissionRequired') and ensure the same
keys are added to the src/translations dictionary files; update the Alert.alert
calls in livekit-store.ts (the microphone permission block, the
voipServerWebsocketSslAddress check, and the token check) to use t('...') for
both title and message instead of raw English text.


// Disconnect from current room if connected (use full cleanup flow)
if (currentRoom) {
currentRoom.disconnect();
await get().disconnectFromRoom();
}

set({ isConnecting: true });

// Start the native audio session before connecting (required for production builds)
// In dev builds, the audio session may persist across hot reloads, but in production
// cold starts it must be explicitly started for WebRTC to function correctly
if (Platform.OS !== 'web') {
try {
await AudioSession.startAudioSession();
logger.info({
message: 'Audio session started successfully',
});
} catch (audioSessionError) {
logger.warn({
message: 'Failed to start audio session - continuing with connection attempt',
context: { error: audioSessionError },
});
}
}

// Create a new room
const room = new Room();

Expand Down Expand Up @@ -576,10 +617,30 @@ export const useLiveKitStore = create<LiveKitState>((set, get) => ({
} catch (error) {
logger.error({
message: 'Failed to connect to room',
context: { error },
context: { error, roomName: roomInfo?.Name },
});

// Stop audio session on failure since we started it above
if (Platform.OS !== 'web') {
try {
await AudioSession.stopAudioSession();
} catch (stopError) {
logger.warn({
message: 'Failed to stop audio session after connection error',
context: { error: stopError },
});
}
}
Comment on lines 618 to +633
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the structure of livekit-store.ts and find the connectToRoom method
fd -a "livekit-store.ts" src/

Repository: Resgrid/Unit

Length of output: 106


🏁 Script executed:

# Get the full connectToRoom method to understand the complete flow
wc -l src/stores/app/livekit-store.ts

Repository: Resgrid/Unit

Length of output: 91


🏁 Script executed:

# Search for the connectToRoom method and its context
rg -n "connectToRoom|async.*connectToRoom" src/stores/app/livekit-store.ts -A 150 | head -200

Repository: Resgrid/Unit

Length of output: 8313


🏁 Script executed:

# Check for any connected flag or state tracking
rg -n "connected|isConnected|connectionStatus" src/stores/app/livekit-store.ts -B 2 -A 2

Repository: Resgrid/Unit

Length of output: 3428


🏁 Script executed:

# Look at the audio service for error handling as indicated in the original request
fd -a "audio.service" src/

Repository: Resgrid/Unit

Length of output: 380


🏁 Script executed:

# Get the complete connectToRoom method from line 372 onwards
sed -n '372,640p' src/stores/app/livekit-store.ts | cat -n

Repository: Resgrid/Unit

Length of output: 12776


🏁 Script executed:

# Check the audio.service.ts for error handling
cat -n src/services/audio.service.ts | head -100

Repository: Resgrid/Unit

Length of output: 4057


🏁 Script executed:

# Get the complete catch block to see all error handling
sed -n '617,680p' src/stores/app/livekit-store.ts | cat -n

Repository: Resgrid/Unit

Length of output: 2477


🏁 Script executed:

# Find and inspect setupAudioRouting function
rg -n "setupAudioRouting" src/stores/app/livekit-store.ts -B 2 -A 20

Repository: Resgrid/Unit

Length of output: 2265


🏁 Script executed:

# Check audio.service.ts for error handling in playConnectToAudioRoomSound
rg -n "playConnectToAudioRoomSound" src/services/audio.service.ts -B 2 -A 15

Repository: Resgrid/Unit

Length of output: 774


🏁 Script executed:

# Find playSound implementation in audio.service.ts
rg -n "playSound" src/services/audio.service.ts -B 2 -A 20 | head -100

Repository: Resgrid/Unit

Length of output: 2295


Consider guarding post-connect side effects from room connection status.

The outer try/catch will treat any later error (audio routing, sound playback) as a connection failure and show a misleading "Connection Failed" alert, even though the room is already connected and set to isConnected: true. While most operations are currently guarded (setupAudioRouting, foreground service, and CallKeep all have inner try/catch blocks), playConnectToAudioRoomSound() delegates to playSound() which silently logs failures without re-throwing. However, future post-connect operations without inner error handling would trigger the false-failure behavior. Consider either:

  • Wrapping each post-connect operation individually to prevent false-failure alerts, or
  • Using an explicit connection confirmation flag to distinguish actual connect failures from post-connect errors
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/app/livekit-store.ts` around lines 618 - 633, The connection error
handler is too broad and treats post-connect failures (e.g., audio routing,
sound playback) as a room connection failure; fix by distinguishing true connect
failures from post-connect side-effect errors: after the LiveKit connect step
completes successfully set an explicit flag (e.g., connectionConfirmed or set
isConnected = true) before running post-connect operations and move the
logger.error/connection-failure alert to only trigger when that flag is not set,
and/or wrap each post-connect call (playConnectToAudioRoomSound -> playSound,
AudioSession.stopAudioSession calls, setupAudioRouting, foreground service,
CallKeep) in their own try/catch so they log warnings without re-throwing or
triggering the "Failed to connect to room" path.


set({ isConnecting: false });

// Show user-visible error so the failure is not silent in production builds
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
Alert.alert(
'Voice Connection Failed',
`Unable to connect to voice channel "${roomInfo?.Name || 'Unknown'}". ${errorMessage}`,
[{ text: 'OK' }]
);
}
},

Expand All @@ -589,6 +650,21 @@ export const useLiveKitStore = create<LiveKitState>((set, get) => ({
await currentRoom.disconnect();
await audioService.playDisconnectedFromAudioRoomSound();

// Stop the native audio session that was started during connectToRoom
if (Platform.OS !== 'web') {
try {
await AudioSession.stopAudioSession();
logger.debug({
message: 'Audio session stopped',
});
} catch (audioSessionError) {
logger.warn({
message: 'Failed to stop audio session',
context: { error: audioSessionError },
});
}
}

// End CallKeep call (works on all platforms - web has no-op implementation)
try {
await callKeepService.endCall();
Expand Down
Loading