Skip to content

CU-868f4q2qr Working on PTT for ios and push notification sounds.#158

Merged
ucswift merged 3 commits intomasterfrom
develop
Aug 16, 2025
Merged

CU-868f4q2qr Working on PTT for ios and push notification sounds.#158
ucswift merged 3 commits intomasterfrom
develop

Conversation

@ucswift
Copy link
Copy Markdown
Member

@ucswift ucswift commented Aug 16, 2025

Summary by CodeRabbit

  • New Features

    • iOS CallKit integration for LiveKit calls (start/end handling) and added native call dependency.
    • Localized, improved Audio Device Selection UI with current-selection summary.
  • Style

    • Better keyboard handling and autofill for login credentials; increased spacing above Server URL button.
    • Room name fallback and layout spacer adjustments for consistent rendering.
  • Tests

    • Extensive unit tests and mocks for call handling, LiveKit store, audio device selection, and login sheet.

@ucswift
Copy link
Copy Markdown
Member Author

ucswift commented Aug 16, 2025

Task linked: CU-868f4q2qr Fix issues with Call Images

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 16, 2025

Walkthrough

Adds iOS CallKit integration and mocks, LiveKit store CallKeep wiring and tests, KeyboardAvoidingView and input hints in login settings, a localized audio device selection UI with tests and translations, a bluetooth-audio-store type fix, a small login layout tweak, and a runtime dependency on react-native-callkeep.

Changes

Cohort / File(s) Summary of changes
CallKeep mock & dependency
__mocks__/react-native-callkeep.ts, package.json
New Jest mock for react-native-callkeep exporting mocked methods and empty constants; adds dependency react-native-callkeep@^4.3.16.
iOS CallKeep service & tests
src/services/callkeep.service.ios.ts, src/services/__tests__/callkeep.service.ios.test.ts
New CallKeepService singleton and CallKeepConfig interface implementing setup, startCall, endCall, isCallActiveNow, getCurrentCallUUID, cleanup; iOS-gated behavior, event listeners, UUID generation, and comprehensive tests.
LiveKit store integration & tests
src/features/livekit-call/store/useLiveKitCallStore.ts, src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
Adds iOS-gated CallKeep wiring to LiveKit store with new public action setupCallKeep() and _setIsConnecting; structured logging, start/end CallKeep calls on connect/disconnect, teardown handling, and extensive tests covering lifecycle, platform gating, mic control, and error paths.
App store tests (CallKeep)
src/stores/app/__tests__/livekit-store.test.ts
Adds CallKeep mock usage and a test suite validating availability, setup, start/end lifecycle, platform gating, and error propagation (duplicate test block present).
Login settings UI & tests
src/components/settings/login-info-bottom-sheet.tsx, src/components/settings/__tests__/login-info-bottom-sheet.test.tsx, src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
Wraps credential fields with KeyboardAvoidingView (iOS padding, height otherwise); adds input attributes (autoCapitalize, autoCorrect, textContentType, autoComplete); extensive unit tests and mocks for rendering and interactions.
Login form tweak
src/app/login/login-form.tsx
Adjusted server URL button top margin from mt-4 to mt-14.
Audio device selection UI, store change & tests
src/components/settings/audio-device-selection.tsx, src/components/settings/__tests__/audio-device-selection.test.tsx, src/stores/app/bluetooth-audio-store.ts
New i18n-enabled AudioDeviceSelection component (localized labels, device-type mapping, selection summary, dark-mode styling), comprehensive tests, and fixed default speaker device type from 'default''speaker'.
Translations added
src/translations/en.json, src/translations/es.json, src/translations/ar.json
Add settings.audio_device_selection translation group with keys for device labels, titles, empty states, and availability in English, Spanish, and Arabic.
LiveKit UI minor tweaks
src/components/livekit/livekit-bottom-sheet.tsx
Defensive rendering for room name fallback and replaced inline spacer with named styles.spacer.
Tests using CallKeep mocks
src/features/..., src/stores/... (various tests)
Multiple tests and test suites updated to require/import the CallKeep mock and reset/mock its methods where needed.

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI (Join/Leave)
  participant Store as useLiveKitCallStore
  participant LK as LiveKit Room
  participant CK as CallKeepService (iOS)
  participant RN as RN CallKeep

  UI->>Store: setupCallKeep()
  alt iOS
    Store->>CK: setup(config)
    CK->>RN: setup(ios/android options)
    RN-->>CK: resolved
  else non-iOS
    Store-->>UI: no-op
  end

  UI->>Store: connectToRoom(roomId)
  Store->>LK: connect(token)
  LK-->>Store: connected
  alt iOS
    Store->>CK: startCall(roomId)
    CK->>RN: startCall(uuid, handle, ...)
    RN-->>CK: ok
  end

  UI->>Store: disconnectFromRoom()
  Store->>LK: disconnect()
  LK-->>Store: disconnected
  alt iOS
    Store->>CK: endCall()
    CK->>RN: endCall(uuid)
    RN-->>CK: ok
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I hop through code with tiny feet,
I mock the bell and bind the beat.
Calls rise on iOS, UUIDs in flight,
Keys, keys, and keyboards tucking night.
Translations bloom — tests hum bright. 🐇📞

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (28)
src/app/login/login-form.tsx (2)

176-180: Use ternary instead of && for conditional rendering (project guideline)

The codebase guideline prefers the conditional operator over &&. Replace the && block with a ternary.

-        {onServerUrlPress && (
+        {onServerUrlPress ? (
           <Button className="mt-14 w-full" variant="outline" action="secondary" onPress={onServerUrlPress}>
             <ButtonText>{t('settings.server_url')}</ButtonText>
           </Button>
-        )}
+        ) : null}

177-177: Spacing change to mt-14: double-check small-screen layout

Bumping to mt-14 significantly increases vertical spacing. Ensure this doesn’t push the button below the fold on smaller devices.

src/components/settings/login-info-bottom-sheet.tsx (3)

56-58: Good call adding KeyboardAvoidingView; consider a keyboardVerticalOffset

On iOS, fixed headers/sheet handles often require an offset to prevent input occlusion. Use safe area inset to compute a reliable offset.

-        <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} className="w-full">
+        <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={top + 64} className="w-full">

Additionally, add this import and hook:

// At top-level imports
import { useSafeAreaInsets } from 'react-native-safe-area-context';

// Inside component (near other hooks)
const { top } = useSafeAreaInsets();

68-71: Improve UX: submit on Return and hint IME

Let users submit from the username field via the keyboard. This also helps tests and accessibility.

-                    <InputField value={value} onChangeText={onChange} placeholder={t('settings.enter_username')} autoCapitalize="none" autoCorrect={false} textContentType="username" autoComplete="username" />
+                    <InputField
+                      value={value}
+                      onChangeText={onChange}
+                      placeholder={t('settings.enter_username')}
+                      autoCapitalize="none"
+                      autoCorrect={false}
+                      textContentType="username"
+                      autoComplete="username"
+                      returnKeyType="next"
+                      onSubmitEditing={() => {/* optionally focus password field here */}}
+                    />

85-93: Password field: ensure submit on Return and secure entry across platforms

Type="password" is great; adding secureTextEntry guards Android/older RN implementations. Also enable submitting on Return.

                     <InputField
                       value={value}
                       onChangeText={onChange}
                       placeholder={t('settings.enter_password')}
                       type="password"
                       autoCapitalize="none"
                       autoCorrect={false}
                       textContentType="password"
                       autoComplete="password"
+                      secureTextEntry
+                      returnKeyType="done"
+                      onSubmitEditing={handleSubmit(onFormSubmit)}
                     />
__mocks__/react-native-callkeep.ts (1)

2-6: Align mock method signatures with library (sync vs async)

In the real API, several methods (e.g., setup, startCall, reportConnecting..., reportConnected..., endCall) are synchronous. Using mockResolvedValue makes them async in tests unnecessarily.

-  setup: jest.fn().mockResolvedValue(undefined),
-  startCall: jest.fn().mockResolvedValue(undefined),
-  reportConnectingOutgoingCallWithUUID: jest.fn().mockResolvedValue(undefined),
-  reportConnectedOutgoingCallWithUUID: jest.fn().mockResolvedValue(undefined),
-  endCall: jest.fn().mockResolvedValue(undefined),
+  setup: jest.fn(),
+  startCall: jest.fn(),
+  reportConnectingOutgoingCallWithUUID: jest.fn(),
+  reportConnectedOutgoingCallWithUUID: jest.fn(),
+  endCall: jest.fn(),
src/services/callkeep.service.ios.ts (5)

53-75: Consider awaiting setup to guarantee readiness before use

If RNCallKeep.setup resolves asynchronously on some platforms/versions, awaiting it ensures listeners/state are initialized deterministically before startCall is invoked.

-      RNCallKeep.setup(options);
+      await RNCallKeep.setup(options as any);
       this.setupEventListeners();
       this.isSetup = true;

106-113: Preemptively end any existing call if a UUID exists (covers in-progress state)

The current check only ends when isCallActive is true. If startCall is called twice quickly, the first call may not have flipped isCallActive yet, leaving an orphaned system call.

-    if (this.isCallActive && currentCallUUID) {
+    if (currentCallUUID) {
       logger.debug({
-        message: 'Call already active, ending existing call first',
+        message: 'Existing call UUID found, ending before starting a new one',
         context: { currentCallUUID },
       });
       await this.endCall();
     }

270-276: Use a stronger UUID source when available

Prefer crypto.randomUUID() when available; fall back to the current impl.

   private generateUUID(): string {
-    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
-      const r = (Math.random() * 16) | 0;
-      const v = c === 'x' ? r : (r & 0x3) | 0x8;
-      return v.toString(16);
-    });
+    // RN 0.76 typically provides global crypto.randomUUID via Hermes/JSI
+    const rndUUID = (global as any)?.crypto?.randomUUID?.();
+    if (typeof rndUUID === 'string') return rndUUID;
+    // Fallback
+    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+      const r = (Math.random() * 16) | 0;
+      const v = c === 'x' ? r : (r & 0x3) | 0x8;
+      return v.toString(16);
+    });
   }

7-9: Optional: make currentCallUUID an instance field

The singleton already scopes state; moving currentCallUUID into the class improves encapsulation and testability.

If you’d like, I can provide a patch to convert currentCallUUID into a private instance property and adjust references accordingly.


3-3: Nit: remove unused imports

AudioSessionCategoryOption, AudioSessionMode, and CK_CONSTANTS are imported but not used.

-import RNCallKeep, { AudioSessionCategoryOption, AudioSessionMode, CONSTANTS as CK_CONSTANTS } from 'react-native-callkeep';
+import RNCallKeep from 'react-native-callkeep';
src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx (3)

2-5: Consolidate Platform mocking and preserve React Native primitives

You’re mocking Platform twice (once via the internal RN path and again via a full react-native mock), and the latter replaces the entire RN module without preserving core components like View/Text/TextInput. This can introduce brittle behavior and break host components. Prefer a single react-native mock that spreads the real module and overrides only what’s needed.

Apply this diff to remove the internal Platform mock and switch to a safe, consolidated mock that preserves RN primitives:

-// Mock Platform first, before any other imports
-jest.mock('react-native/Libraries/Utilities/Platform', () => ({
-  OS: 'ios',
-  select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
-}));
+// Consolidated RN mock that preserves actual components and overrides only what's needed

@@
-jest.mock('react-native', () => ({
-  useWindowDimensions: () => ({
-    width: 400,
-    height: 800,
-  }),
-  Platform: {
-    OS: 'ios',
-    select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
-  },
-  KeyboardAvoidingView: ({ children, ...props }: any) => {
-    const React = require('react');
-    return React.createElement('View', { testID: 'keyboard-avoiding-view', ...props }, children);
-  },
-}));
+jest.mock('react-native', () => {
+  const RN = jest.requireActual('react-native');
+  return {
+    ...RN,
+    useWindowDimensions: () => ({
+      width: 400,
+      height: 800,
+    }),
+    Platform: {
+      ...RN.Platform,
+      OS: 'ios',
+      select: jest.fn().mockImplementation((obj) => obj.ios ?? obj.default),
+    },
+    KeyboardAvoidingView: ({ children, ...props }: any) => {
+      const React = require('react');
+      return React.createElement(RN.View, { testID: 'keyboard-avoiding-view', ...props }, children);
+    },
+  };
+});

Also applies to: 25-38


214-221: Make the Cancel press assertion less brittle

Accessing .parent of the text node relies on test-renderer internals. Prefer selecting the interactive element directly. Since the Button mock already forwards props, expose a testID on the cancel button for a stable query.

For example, in the component, set testID="cancel-button" on the Cancel Button. Then in the test:

-const cancelButton = screen.getByText('common.cancel').parent;
-fireEvent.press(cancelButton);
+fireEvent.press(screen.getByTestId('cancel-button'));

If you don’t want to touch the component, at least query the nearest “button” testID instance instead of relying on .parent.


1-223: Avoid test duplication with the sibling test file

This test file appears effectively identical to src/components/settings/tests/login-info-bottom-sheet.test.tsx. Duplicated suites increase runtime and maintenance load without adding coverage. Consider keeping only one suite and moving the shared mocking to a small test utility for reuse.

I can help factor the shared mocks into a reusable test utils module if you want.

src/components/settings/__tests__/login-info-bottom-sheet.test.tsx (3)

2-5: Unify RN mocking to avoid brittle behavior

Same concern as the “simple” variant: double-mocking Platform and fully replacing the RN module risks breaking primitives. Spread the actual RN module and override only what’s needed.

Apply the same consolidated mock approach suggested in the sibling file to this one as well.

Also applies to: 25-38


214-221: Press handler selection could be more robust

Using .parent to find the pressable is brittle. Prefer attaching a testID to the Cancel button and firing the press on it directly, or enhance the Button mock to accept and forward an accessibilityRole="button" and query by role.


1-223: Test duplication with the “simple” suite

This file largely duplicates src/components/settings/tests/login-info-bottom-sheet-simple.test.tsx. Please consolidate to a single suite and extract the common mock scaffolding.

src/features/livekit-call/store/useLiveKitCallStore.ts (4)

5-6: Don’t import an iOS-specific file from cross-platform store code

Importing callkeep.service.ios directly in a shared store can complicate Android builds and bundling. Prefer a platform-agnostic import path with .ios.ts and .android.ts siblings (the latter being a no-op stub), or lazy-load inside an iOS guard.

Apply this diff to move to a platform-agnostic import:

-import { callKeepService } from '../../../services/callkeep.service.ios';
+import { callKeepService } from '../../../services/callkeep.service';

And add a stub at src/services/callkeep.service.android.ts:

// src/services/callkeep.service.android.ts
export const callKeepService = {
  setup: async (_config?: unknown) => {},
  startCall: async (_roomId: string, _handle?: string) => '',
  endCall: async () => {},
  isCallActiveNow: () => false,
  getCurrentCallUUID: () => null as string | null,
  cleanup: async () => {},
};

Alternatively, dynamically import under the iOS guard:

if (Platform.OS === 'ios') {
  const { callKeepService } = await import('../../../services/callkeep.service.ios');
  await callKeepService.setup({...});
}

138-148: Start CallKeep call on connect is reasonable, but consider display handle

Using roomId is fine for now. If you have a human-readable room name, consider passing it as the call handle to improve iOS Call UI clarity.


160-170: Potential double endCall on disconnect

You end the CallKeep call both in the ConnectionState.Disconnected handler and again in disconnectFromRoom(). The service should no-op safely when no active call exists, but this can add log noise.

If you prefer to avoid duplicate calls, you can rely solely on one path (e.g., keep it in the ConnectionState handler) or guard with callKeepService.isCallActiveNow():

-// End CallKeep call for iOS
-if (Platform.OS === 'ios') {
-  try {
-    await callKeepService.endCall();
-    console.log('CallKeep call ended successfully');
-  } catch (error) {
-    console.warn('Failed to end CallKeep call:', error);
-  }
-}
+// End CallKeep call for iOS (only if still active to avoid duplicate logs)
+if (Platform.OS === 'ios' && callKeepService.isCallActiveNow()) {
+  try {
+    await callKeepService.endCall();
+    console.log('CallKeep call ended successfully');
+  } catch (error) {
+    console.warn('Failed to end CallKeep call:', error);
+  }
+}

Also applies to: 248-256


103-129: Prefer project logger over console for consistency

Elsewhere you’re using a structured logger; using console.* here diverges from that pattern and can reduce observability in production.

If you have a logger at src/lib/logging, import and replace console.*:

+import { logger } from '../../../lib/logging';

- console.warn('Connection attempt while already connecting or connected.');
+ logger.warn?.({ message: 'Connection attempt while already connecting or connected.' });

- console.log('LiveKit Connection State Changed:', state);
+ logger.debug?.({ message: 'LiveKit Connection State Changed', context: { state } });

- console.error('Failed to connect to LiveKit room:', err);
+ logger.error?.({ message: 'Failed to connect to LiveKit room', context: { error: err } });

Also applies to: 200-206, 215-228

src/stores/app/__tests__/livekit-store.test.ts (2)

122-127: Preserve RN module and override only Platform

Mocking the entire react-native module with only Platform can break other imports if this test grows. Safer to spread the actual module and override Platform.OS.

Apply this diff:

-jest.mock('react-native', () => ({
-  Platform: {
-    OS: 'android',
-  },
-}));
+jest.mock('react-native', () => {
+  const RN = jest.requireActual('react-native');
+  return {
+    ...RN,
+    Platform: { ...RN.Platform, OS: 'android' },
+  };
+});

382-451: “CallKeep Integration” block isn’t exercising store behavior

These tests confirm the mock’s shape and basic calls, but they don’t exercise any behavior of the store under test. Consider moving this block to the CallKeep service tests (where it’s more relevant) or reworking it to invoke store actions that actually use CallKeep (e.g., setupCallKeep() or connect/disconnect flows in the LiveKit call store).

To make this meaningful here, assert platform gating by calling the relevant store action and verifying the mock wasn’t invoked on Android:

-it('should skip CallKeep operations on non-iOS platforms', async () => {
-  (Platform as any).OS = 'android';
-  // Verify that platform checks work as expected
-  expect(Platform.OS).toBe('android');
-  // CallKeep operations would be skipped on Android
-  // This test confirms the platform detection works properly
-});
+it('should skip setupCallKeep on Android', async () => {
+  (Platform as any).OS = 'android';
+  const { setupCallKeep } = require('../../../features/livekit-call/store/useLiveKitCallStore').useLiveKitCallStore.getState().actions;
+  await setupCallKeep();
+  expect(mockCallKeepService.setup).not.toHaveBeenCalled();
+});
src/services/__tests__/callkeep.service.ios.test.ts (1)

244-255: Singleton tests look good; consider a cleanup test

As an optional enhancement, add a test for cleanup() to ensure event listeners are removed and internal flags reset.

I can draft this if useful.

src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (4)

56-57: Inconsistent variable naming.

The variable originalEnv is declared outside the beforeAll hook but only used within test lifecycle hooks. This same pattern is repeated in the error handling test (line 513).

Move the declaration inside beforeAll:

 describe('useLiveKitCallStore with CallKeep Integration', () => {
-  // Mock environment variable for successful token fetching
-  const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
   
   beforeAll(() => {
+    const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
     process.env.STORYBOOK_LIVEKIT_TOKEN = 'mock-test-token';
-  });
-  
-  afterAll(() => {
-    if (originalEnv) {
-      process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
-    } else {
-      delete process.env.STORYBOOK_LIVEKIT_TOKEN;
-    }
+    
+    return () => {
+      if (originalEnv) {
+        process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
+      } else {
+        delete process.env.STORYBOOK_LIVEKIT_TOKEN;
+      }
+    };
   });

147-155: Mock implementation doesn't cover all event types.

The mock only handles connectionStateChanged events, but the store implementation subscribes to multiple RoomEvent types. This could lead to incomplete test coverage or missed edge cases.

Consider implementing a more comprehensive mock that handles all event types:

     beforeEach(() => {
       // Mock successful connection flow
+      const eventListeners = new Map<string, Function>();
       mockRoom.on.mockImplementation((event: any, callback: any) => {
-        if (event === 'connectionStateChanged') {
-          // Simulate connected state
-          setTimeout(() => callback('connected'), 0);
-        }
+        eventListeners.set(event, callback);
+        // Simulate connected state for connectionStateChanged
+        if (event === 'connectionStateChanged') {
+          setTimeout(() => callback('connected'), 0);
+        }
         return mockRoom;
       });
     });

184-200: Test doesn't verify that the error is properly set in the store.

The test handles CallKeep start call errors but only verifies console output. It should also check if the error is reflected in the store state since the implementation seems to continue despite the error.

Add assertion to verify the store's error state remains unaffected (since this is a non-critical error):

       expect(consoleSpy).toHaveBeenCalledWith(
         'Failed to start CallKeep call (background audio may not work):',
         error
       );
+      // Verify that the error doesn't affect the store state (non-critical error)
+      expect(result.current.error).toBeNull();
+      expect(result.current.isConnected).toBe(true);
       
       consoleSpy.mockRestore();

300-307: Test relies on timing assumptions that could cause flakiness.

The test checks if connectionStateListener is defined and then calls it, but there's a potential race condition if the mock implementation changes or if the async operations take longer than expected.

Add a more robust wait mechanism:

       expect(connectionStateListener).toBeDefined();
       
       // Simulate disconnection
-      if (connectionStateListener) {
-        act(() => {
-          connectionStateListener!('disconnected');
-        });
-      }
+      await act(async () => {
+        if (!connectionStateListener) {
+          throw new Error('Connection state listener was not set up');
+        }
+        connectionStateListener('disconnected');
+        // Wait for async operations to complete
+        await new Promise(resolve => setTimeout(resolve, 10));
+      });
 
       expect(mockCallKeepService.endCall).toHaveBeenCalled();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1979ac0 and e8c9b7f.

⛔ Files ignored due to path filters (47)
  • .DS_Store is excluded by !**/.DS_Store
  • assets/audio/callclosed.wav is excluded by !**/*.wav
  • assets/audio/callemergency.wav is excluded by !**/*.wav
  • assets/audio/callhigh.wav is excluded by !**/*.wav
  • assets/audio/calllow.wav is excluded by !**/*.wav
  • assets/audio/callmedium.wav is excluded by !**/*.wav
  • assets/audio/callupdated.wav is excluded by !**/*.wav
  • assets/audio/custom/c1.wav is excluded by !**/*.wav
  • assets/audio/custom/c10.wav is excluded by !**/*.wav
  • assets/audio/custom/c11.wav is excluded by !**/*.wav
  • assets/audio/custom/c12.wav is excluded by !**/*.wav
  • assets/audio/custom/c13.wav is excluded by !**/*.wav
  • assets/audio/custom/c14.wav is excluded by !**/*.wav
  • assets/audio/custom/c15.wav is excluded by !**/*.wav
  • assets/audio/custom/c16.wav is excluded by !**/*.wav
  • assets/audio/custom/c17.wav is excluded by !**/*.wav
  • assets/audio/custom/c18.wav is excluded by !**/*.wav
  • assets/audio/custom/c19.wav is excluded by !**/*.wav
  • assets/audio/custom/c2.wav is excluded by !**/*.wav
  • assets/audio/custom/c20.wav is excluded by !**/*.wav
  • assets/audio/custom/c21.wav is excluded by !**/*.wav
  • assets/audio/custom/c22.wav is excluded by !**/*.wav
  • assets/audio/custom/c23.wav is excluded by !**/*.wav
  • assets/audio/custom/c24.wav is excluded by !**/*.wav
  • assets/audio/custom/c25.wav is excluded by !**/*.wav
  • assets/audio/custom/c3.wav is excluded by !**/*.wav
  • assets/audio/custom/c4.wav is excluded by !**/*.wav
  • assets/audio/custom/c5.wav is excluded by !**/*.wav
  • assets/audio/custom/c6.wav is excluded by !**/*.wav
  • assets/audio/custom/c7.wav is excluded by !**/*.wav
  • assets/audio/custom/c8.wav is excluded by !**/*.wav
  • assets/audio/custom/c9.wav is excluded by !**/*.wav
  • assets/audio/newcall.wav is excluded by !**/*.wav
  • assets/audio/newchat.wav is excluded by !**/*.wav
  • assets/audio/newmessage.wav is excluded by !**/*.wav
  • assets/audio/newshift.wav is excluded by !**/*.wav
  • assets/audio/newtraining.wav is excluded by !**/*.wav
  • assets/audio/notification.wav is excluded by !**/*.wav
  • assets/audio/personnelstaffingupdated.wav is excluded by !**/*.wav
  • assets/audio/personnelstatusupdated.wav is excluded by !**/*.wav
  • assets/audio/troublealert.wav is excluded by !**/*.wav
  • assets/audio/unitnotice.wav is excluded by !**/*.wav
  • assets/audio/unitstatusupdated.wav is excluded by !**/*.wav
  • assets/audio/upcomingshift.wav is excluded by !**/*.wav
  • assets/audio/upcomingtraining.wav is excluded by !**/*.wav
  • assets/icon.png is excluded by !**/*.png
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (11)
  • __mocks__/react-native-callkeep.ts (1 hunks)
  • package.json (1 hunks)
  • src/app/login/login-form.tsx (1 hunks)
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx (1 hunks)
  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx (1 hunks)
  • src/components/settings/login-info-bottom-sheet.tsx (2 hunks)
  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (1 hunks)
  • src/features/livekit-call/store/useLiveKitCallStore.ts (6 hunks)
  • src/services/__tests__/callkeep.service.ios.test.ts (1 hunks)
  • src/services/callkeep.service.ios.ts (1 hunks)
  • src/stores/app/__tests__/livekit-store.test.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names (e.g., isFetchingData, handleUserInput)
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Use Expo SecureStore for sensitive data
Use zustand for state management
Use react-query for data fetching and caching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Use React Navigation for handling navigation and deep linking
Handle errors gracefully and provide user feedback
Use Expo's SecureStore for sensitive data
Use zustand for state management
Use react-query for data fetching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

Files:

  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx
  • src/app/login/login-form.tsx
  • src/services/__tests__/callkeep.service.ios.test.ts
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
  • src/components/settings/login-info-bottom-sheet.tsx
  • __mocks__/react-native-callkeep.ts
  • src/services/callkeep.service.ios.ts
  • src/stores/app/__tests__/livekit-store.test.ts
  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
  • src/features/livekit-call/store/useLiveKitCallStore.ts
**/*.tsx

📄 CodeRabbit Inference Engine (.cursorrules)

**/*.tsx: Use functional components and hooks over class components
Use PascalCase for React component names (e.g., UserProfile, ChatScreen)
Utilize React.FC for defining functional components with props
Minimize useEffect, useState, and heavy computations inside render
Use React.memo() for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, windowSize
Use getItemLayout for FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers to prevent re-renders
Use gluestack-ui for styling; if no component exists in src/components/ui, style via StyleSheet.create or styled-components
Optimize images using react-native-fast-image
Use React Navigation for navigation and deep linking with best practices
Wrap all user-facing text in t() from react-i18next
Use react-hook-form for form handling
Use @rnmapbox/maps for maps or vehicle navigation
Use lucide-react-native for icons directly in markup; do not use the gluestack-ui icon component
Use the conditional operator (?:) for conditional rendering instead of &&

**/*.tsx: Use functional components and hooks over class components
Use PascalCase for component names
Utilize React.FC for defining functional components with props
Minimize useEffect/useState and heavy computations inside render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Use getItemLayout for FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers
Optimize image handling using react-native-fast-image
Wrap all user-facing text in t() from react-i18next
Ensure support for dark mode and light mode
Ensure the app is accessible following WCAG for mobile
Use react-hook-form for form handling
Use @rnmapbox/maps for maps and navigation
Use lucide-react-native for icons directly in markup; do not us...

Files:

  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx
  • src/app/login/login-form.tsx
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
  • src/components/settings/login-info-bottom-sheet.tsx
**/*

📄 CodeRabbit Inference Engine (.cursorrules)

Directory and file names should be lowercase and hyphenated (e.g., user-profile, chat-screen)

Files:

  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx
  • src/app/login/login-form.tsx
  • package.json
  • src/services/__tests__/callkeep.service.ios.test.ts
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
  • src/components/settings/login-info-bottom-sheet.tsx
  • __mocks__/react-native-callkeep.ts
  • src/services/callkeep.service.ios.ts
  • src/stores/app/__tests__/livekit-store.test.ts
  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
  • src/features/livekit-call/store/useLiveKitCallStore.ts
**/*.{test.ts,test.tsx,spec.ts,spec.tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

Create and use Jest tests to validate all generated components

Files:

  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx
  • src/services/__tests__/callkeep.service.ios.test.ts
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
  • src/stores/app/__tests__/livekit-store.test.ts
  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
**/__tests__/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

Generate tests for all components, services, and logic; ensure tests run without errors

Files:

  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx
  • src/services/__tests__/callkeep.service.ios.test.ts
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
  • src/stores/app/__tests__/livekit-store.test.ts
  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
{components/ui/**/*.{ts,tsx},**/*.tsx}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Use gluestack-ui consistently; if no component exists in components/ui, style via StyleSheet.create or styled-components

Files:

  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx
  • src/app/login/login-form.tsx
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
  • src/components/settings/login-info-bottom-sheet.tsx
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Create and use Jest tests for all generated components

Files:

  • src/components/settings/__tests__/login-info-bottom-sheet.test.tsx
  • src/services/__tests__/callkeep.service.ios.test.ts
  • src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx
  • src/stores/app/__tests__/livekit-store.test.ts
  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
🧠 Learnings (1)
📚 Learning: 2025-08-12T03:33:40.227Z
Learnt from: CR
PR: Resgrid/Unit#0
File: .cursorrules:0-0
Timestamp: 2025-08-12T03:33:40.227Z
Learning: Applies to **/*.tsx : Use react-hook-form for form handling

Applied to files:

  • src/components/settings/login-info-bottom-sheet.tsx
🧬 Code Graph Analysis (8)
src/components/settings/__tests__/login-info-bottom-sheet.test.tsx (1)
src/components/settings/login-info-bottom-sheet.tsx (1)
  • LoginInfoBottomSheet (25-112)
src/app/login/login-form.tsx (1)
src/components/ui/button/index.tsx (1)
  • Button (334-334)
src/services/__tests__/callkeep.service.ios.test.ts (2)
src/lib/logging/index.tsx (1)
  • logger (80-80)
src/services/callkeep.service.ios.ts (2)
  • CallKeepService (19-310)
  • callKeepService (313-313)
src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx (1)
src/components/settings/login-info-bottom-sheet.tsx (1)
  • LoginInfoBottomSheet (25-112)
src/components/settings/login-info-bottom-sheet.tsx (2)
src/components/ui/input/index.tsx (2)
  • Input (174-174)
  • InputField (174-174)
src/components/ui/button/index.tsx (3)
  • Button (334-334)
  • ButtonText (334-334)
  • ButtonSpinner (334-334)
src/services/callkeep.service.ios.ts (1)
src/lib/logging/index.tsx (2)
  • logger (80-80)
  • error (74-76)
src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (1)
src/features/livekit-call/store/useLiveKitCallStore.ts (1)
  • useLiveKitCallStore (70-303)
src/features/livekit-call/store/useLiveKitCallStore.ts (1)
src/services/callkeep.service.ios.ts (1)
  • callKeepService (313-313)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (14)
package.json (1)

147-147: Ensure iOS infoPlist includes microphone usage and verify VoIP mode

Your app.config.ts already defines UIBackgroundModes (remote-notification, audio, bluetooth-central, voip) and NSBluetoothAlwaysUsageDescription, but you’re missing the required microphone permission string and should confirm whether you really need “voip” background mode.

• File: app.config.ts (around lines 47–51)
– Add
"NSMicrophoneUsageDescription": "Explain why your app needs microphone access for calls."
– If you’re not using PushKit/PKPushRegistry for incoming VoIP pushes, remove "voip" from UIBackgroundModes and keep only ["audio"] to avoid App Store issues.

Suggested diff:

 infoPlist: {
-  UIBackgroundModes: ['remote-notification','audio','bluetooth-central','voip'],
+  UIBackgroundModes: ['remote-notification','audio','bluetooth-central'], 
+  NSMicrophoneUsageDescription: 'Your app needs microphone access to make and receive calls.',
   ITSAppUsesNonExemptEncryption: false,
   NSBluetoothAlwaysUsageDescription: 'Allow Resgrid Unit to connect to bluetooth devices for PTT.',
 },

Likely an incorrect or invalid review comment.

src/components/settings/login-info-bottom-sheet.tsx (1)

56-109: LGTM: Nice keyboard handling and input hints

The KeyboardAvoidingView and text input hints (autoCapitalize/autoComplete/textContentType) materially improve usability. Button sizing by orientation is a nice touch.

__mocks__/react-native-callkeep.ts (1)

1-13: LGTM: Comprehensive CallKeep mock

The mock covers the surface needed by the service and tests and exports named constants to satisfy imports.

src/components/settings/__tests__/login-info-bottom-sheet-simple.test.tsx (2)

143-166: Solid coverage of rendering and props

Good assertions on Actionsheet presence, KeyboardAvoidingView, and i18n label rendering under the iOS mock. This meaningfully guards the regression surface around recent KAV and input prop changes.


173-184: Nice verification of input field ergonomics

You thoroughly assert the input props for username and password (autoCapitalize, autoCorrect, autoComplete, textContentType, secureTextEntry, placeholders). This is pragmatic coverage for mobile UX concerns.

Also applies to: 186-198

src/components/settings/__tests__/login-info-bottom-sheet.test.tsx (3)

157-165: Good: iOS rendering behavior is asserted

The checks for Actionsheet/Content and KeyboardAvoidingView presence under iOS are on point.


173-184: Good: Input props assertions cover key UX contracts

Verifying username/password props here mirrors the component’s intended ergonomics.

Also applies to: 186-198


200-205: iOS-specific KAV behavior covered

Asserting behavior="padding" under iOS mock helps prevent regressions when platform logic is refactored.

src/features/livekit-call/store/useLiveKitCallStore.ts (1)

82-98: setupCallKeep is correctly gated and error-handled

The iOS guard and try/catch with user-facing error state are appropriate. This makes the action idempotent per the service’s own semantics.

src/stores/app/__tests__/livekit-store.test.ts (1)

335-344: Good: unsupported platform handling verified

Asserting that nothing is called and no logs are emitted on an unsupported platform is pragmatic and keeps the permission logic focused.

src/services/__tests__/callkeep.service.ios.test.ts (4)

97-132: Setup configuration and logging are well-verified

Asserting the full RNCallKeep.setup payload and structured logs (info) provides solid guarantees around initialization behavior.


134-149: Event listener wiring covered

Good checks for all expected listeners, including audio session activation/deactivation and mute state actions.


167-197: Start call flow assertions are thorough

You validate UUID format, RNCallKeep.startCall arguments, and the “connecting” report. The custom handle case is a nice addition.


215-241: End call behaviors are well-exercised

Verifying proper endCall arguments, state reset, and the “No active call to end” path increases confidence in teardown logic.

Comment on lines +513 to +536
const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
delete process.env.STORYBOOK_LIVEKIT_TOKEN;

const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});

const { result } = renderHook(() => useLiveKitCallStore());

await act(async () => {
await result.current.actions.connectToRoom('test-room', 'test-participant');
});

expect(result.current.error).toBe('Failed to fetch a valid connection token.');
expect(result.current.isConnecting).toBe(false);
expect(result.current.isConnected).toBe(false);
expect(result.current.roomInstance).toBeNull();
expect(result.current.currentRoomId).toBeNull();

// Restore original environment
if (originalEnv) {
process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
}

consoleSpy.mockRestore();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Test has cleanup issues with environment variables.

The test manipulates process.env.STORYBOOK_LIVEKIT_TOKEN but could interfere with other tests if it fails before restoration. This is particularly problematic since the describe block's afterAll won't run if this specific test fails.

Use a try-finally block or afterEach hook to ensure cleanup:

     it('should handle token fetch errors', async () => {
       // Mock environment to simulate missing token
       const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
-      delete process.env.STORYBOOK_LIVEKIT_TOKEN;
-      
-      const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
-      
-      const { result } = renderHook(() => useLiveKitCallStore());
-
-      await act(async () => {
-        await result.current.actions.connectToRoom('test-room', 'test-participant');
-      });
-
-      expect(result.current.error).toBe('Failed to fetch a valid connection token.');
-      expect(result.current.isConnecting).toBe(false);
-      expect(result.current.isConnected).toBe(false);
-      expect(result.current.roomInstance).toBeNull();
-      expect(result.current.currentRoomId).toBeNull();
-      
-      // Restore original environment
-      if (originalEnv) {
-        process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
-      }
-      
-      consoleSpy.mockRestore();
+      try {
+        delete process.env.STORYBOOK_LIVEKIT_TOKEN;
+        
+        const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+        
+        const { result } = renderHook(() => useLiveKitCallStore());
+
+        await act(async () => {
+          await result.current.actions.connectToRoom('test-room', 'test-participant');
+        });
+
+        expect(result.current.error).toBe('Failed to fetch a valid connection token.');
+        expect(result.current.isConnecting).toBe(false);
+        expect(result.current.isConnected).toBe(false);
+        expect(result.current.roomInstance).toBeNull();
+        expect(result.current.currentRoomId).toBeNull();
+        
+        consoleSpy.mockRestore();
+      } finally {
+        // Always restore original environment
+        if (originalEnv) {
+          process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
+        }
+      }
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
delete process.env.STORYBOOK_LIVEKIT_TOKEN;
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLiveKitCallStore());
await act(async () => {
await result.current.actions.connectToRoom('test-room', 'test-participant');
});
expect(result.current.error).toBe('Failed to fetch a valid connection token.');
expect(result.current.isConnecting).toBe(false);
expect(result.current.isConnected).toBe(false);
expect(result.current.roomInstance).toBeNull();
expect(result.current.currentRoomId).toBeNull();
// Restore original environment
if (originalEnv) {
process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
}
consoleSpy.mockRestore();
});
it('should handle token fetch errors', async () => {
// Mock environment to simulate missing token
const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
try {
delete process.env.STORYBOOK_LIVEKIT_TOKEN;
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => useLiveKitCallStore());
await act(async () => {
await result.current.actions.connectToRoom('test-room', 'test-participant');
});
expect(result.current.error).toBe('Failed to fetch a valid connection token.');
expect(result.current.isConnecting).toBe(false);
expect(result.current.isConnected).toBe(false);
expect(result.current.roomInstance).toBeNull();
expect(result.current.currentRoomId).toBeNull();
consoleSpy.mockRestore();
} finally {
// Always restore original environment
if (originalEnv) {
process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
}
}
});
🤖 Prompt for AI Agents
In src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts around
lines 513 to 536, the test removes process.env.STORYBOOK_LIVEKIT_TOKEN and sets
up a console spy but only restores them at the end of the test body, which can
leak state if the test throws; wrap the test actions in a try-finally (or move
cleanup to an afterEach) so that in the finally block you always restore
process.env.STORYBOOK_LIVEKIT_TOKEN to the previously saved originalEnv (or
delete it if originalEnv was undefined) and call consoleSpy.mockRestore(),
ensuring no environment or spy leakage even on failures.

Comment on lines +10 to +17
export interface CallKeepConfig {
appName: string;
maximumCallGroups: string;
maximumCallsPerCallGroup: string;
includesCallsInRecents: boolean;
supportsVideo: boolean;
ringtoneSound?: string;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Type mismatch: maximumCallGroups and maximumCallsPerCallGroup should be numbers

RNCallKeep iOS options expect numeric values. Using strings can cause native type mismatches at runtime.

 export interface CallKeepConfig {
   appName: string;
-  maximumCallGroups: string;
-  maximumCallsPerCallGroup: string;
+  maximumCallGroups: number;
+  maximumCallsPerCallGroup: number;
   includesCallsInRecents: boolean;
   supportsVideo: boolean;
   ringtoneSound?: string;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface CallKeepConfig {
appName: string;
maximumCallGroups: string;
maximumCallsPerCallGroup: string;
includesCallsInRecents: boolean;
supportsVideo: boolean;
ringtoneSound?: string;
}
export interface CallKeepConfig {
appName: string;
maximumCallGroups: number;
maximumCallsPerCallGroup: number;
includesCallsInRecents: boolean;
supportsVideo: boolean;
ringtoneSound?: string;
}
🤖 Prompt for AI Agents
In src/services/callkeep.service.ios.ts around lines 10 to 17, the
CallKeepConfig interface declares maximumCallGroups and maximumCallsPerCallGroup
as strings but RNCallKeep iOS expects numbers; change both properties to type
number and update any places that construct or pass this config to ensure
numeric values (parse or convert strings where necessary) so the native bridge
receives numbers, and add/update tests or TypeScript callers to reflect the new
types.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (12)
src/components/livekit/livekit-bottom-sheet.tsx (2)

219-219: Prefer a localized fallback instead of an empty string for room name

Using an empty fallback hides useful context and can be confusing. Align with other UI fallbacks and i18n:

-            <Text className="text-center text-lg font-medium">{currentRoomInfo?.Name || ''}</Text>
+            <Text className="text-center text-lg font-medium">
+              {currentRoomInfo?.Name ?? t('common.unknown')}
+            </Text>

267-268: Avoid fixed-width spacer to keep layout robust across locales and font scales

A hard-coded width can break with long translations or dynamic font sizes. Use an invisible placeholder that matches the actual back label width so the title stays centered consistently.

Apply this minimal change:

-          <View style={styles.spacer} />
+          <Text style={styles.invisibleBackPlaceholder}>{t('common.back')}</Text>

And update the style:

-  spacer: {
-    width: 50,
-  },
+  invisibleBackPlaceholder: {
+    opacity: 0,
+  },

This reserves the exact space of the back label in the current locale without assuming a fixed width.

Also applies to: 354-356

src/components/settings/__tests__/audio-device-selection.test.tsx (4)

148-160: Avoid brittle DOM traversal by using stable testIDs on pressable items

Relying on parent?.parent?.parent is fragile and may break with minor layout changes. Prefer pressing the list item via a stable testID exposed by the component.

Apply this test change after adding a testID to the Pressable (see component review):

-      const { getAllByText } = render(<AudioDeviceSelection />);
-
-      // Find the first device card (should be in microphone section)
-      const deviceCards = getAllByText('Bluetooth Headset');
-      fireEvent.press(deviceCards[0].parent?.parent?.parent as any);
+      render(<AudioDeviceSelection />);
+      const pressable = screen.getByTestId(`audio-device-item-microphone-${bluetoothMic.id}`);
+      fireEvent.press(pressable);

If you want, I can update all affected tests after we add the testID in the component.


162-174: Same here: use testIDs for speaker item press

Mirror the microphone test to avoid layout-coupled traversal.

-      const { getAllByText } = render(<AudioDeviceSelection />);
-
-      // Find the second device card (should be in speaker section)
-      const deviceCards = getAllByText('Bluetooth Speaker');
-      fireEvent.press(deviceCards[1].parent?.parent?.parent as any);
+      render(<AudioDeviceSelection />);
+      const pressable = screen.getByTestId(`audio-device-item-speaker-${bluetoothSpeaker.id}`);
+      fireEvent.press(pressable);

194-205: Add a test to assert the “Unavailable” suffix for non-Bluetooth devices

You cover filtering for unavailable bluetooth devices, but not that non-bluetooth unavailable devices still render with the “(Unavailable)” suffix, as the component currently does.

Consider adding:

+    it('shows "(Unavailable)" suffix for unavailable non-bluetooth devices in microphone list', () => {
+      const wiredUnavailable = createMockDevice('wired-u', 'Wired Mic', 'wired', false);
+      mockStore.availableAudioDevices = [wiredUnavailable];
+      render(<AudioDeviceSelection />);
+      expect(screen.getByText('Wired Mic')).toBeTruthy();
+      expect(screen.getByText(/Unavailable$/)).toBeTruthy();
+    });

271-281: Clarify the unknown type fallback assertion

Right now you assert twice against 'Unknown Device', which coincidentally matches the device name. If the fallback label remains “Unknown Device Device”, assert it explicitly to prove the label path too, or switch the component fallback to an i18n key and assert that.

Example tweak if keeping the current fallback logic:

-      expect(screen.getAllByText('Unknown Device').length).toBeGreaterThan(0);
-      expect(screen.getAllByText('Unknown Device').length).toBeGreaterThan(0);
+      expect(screen.getAllByText('Unknown Device').length).toBeGreaterThan(0); // name
+      expect(screen.getAllByText('Unknown Device Device').length).toBeGreaterThan(0); // type label fallback
src/components/settings/audio-device-selection.tsx (6)

35-46: Tighten the device type typing and handle the 'default' type explicitly

Type the parameter precisely and add a mapping for 'default'. Also consider localizing the fallback instead of concatenating " Device".

-  const getDeviceTypeLabel = (deviceType: string) => {
+  const getDeviceTypeLabel = (deviceType: AudioDeviceInfo['type'] | (string & {})) => {
     switch (deviceType) {
       case 'bluetooth':
         return t('settings.audio_device_selection.bluetooth_device');
       case 'wired':
         return t('settings.audio_device_selection.wired_device');
       case 'speaker':
         return t('settings.audio_device_selection.speaker_device');
+      case 'default':
+        // Add a translation key for this across locales; fallback keeps current UX stable if missing.
+        return 'Default Device';
       default:
-        return deviceType.charAt(0).toUpperCase() + deviceType.slice(1) + ' Device';
+        // Consider introducing a key like `settings.audio_device_selection.unknown_device_type`
+        // and/or avoiding hard-coded English suffixes here.
+        return deviceType.charAt(0).toUpperCase() + deviceType.slice(1) + ' Device';
     }
   };

I can add i18n keys for default/unknown device types in en/es/ar to remove the hard-coded English.


81-84: Follow guideline: use ?: instead of && for conditional rendering

Coding guidelines request the conditional operator over &&.

-        {showTitle && (
-          <Heading size="lg" className="text-gray-900 dark:text-gray-100">
-            <Text>{t('settings.audio_device_selection.title')}</Text>
-          </Heading>
-        )}
+        {showTitle ? (
+          <Heading size="lg" className="text-gray-900 dark:text-gray-100">
+            <Text>{t('settings.audio_device_selection.title')}</Text>
+          </Heading>
+        ) : null}

94-101: Localize punctuation: include the colon in the translation or use a dedicated key

Appending ":" outside t() can be awkward for some locales. Prefer including punctuation/formatting in the translation.

-              <Text className="text-blue-800 dark:text-blue-200">{t('settings.audio_device_selection.microphone')}:</Text>
+              <Text className="text-blue-800 dark:text-blue-200">{t('settings.audio_device_selection.microphone_label')}</Text>
...
-              <Text className="text-blue-800 dark:text-blue-200">{t('settings.audio_device_selection.speaker')}:</Text>
+              <Text className="text-blue-800 dark:text-blue-200">{t('settings.audio_device_selection.speaker_label')}</Text>

I can add microphone_label/speaker_label keys to en/es/ar with a colon.


53-71: Expose stable testIDs and improve accessibility for device items

Add testIDs to Pressable for reliable e2e/unit tests and set proper accessibility props. This avoids brittle DOM traversal in tests and improves screen reader support.

-      <Pressable key={`${deviceType}-${device.id}`} onPress={onSelect}>
+      <Pressable
+        key={`${deviceType}-${device.id}`}
+        testID={`audio-device-item-${deviceType}-${device.id}`}
+        accessibilityRole="button"
+        accessibilityLabel={`${device.name}, ${deviceTypeLabel}${unavailableText ? ` ${t('settings.audio_device_selection.unavailable')}` : ''}`}
+        onPress={onSelect}
+      >
         <Card className={`mb-2 p-4 ${isSelected ? 'border-blue-600 bg-blue-50 dark:border-blue-400 dark:bg-blue-950/30' : 'border-gray-200 dark:border-gray-700'}`}>
           <HStack className="items-center justify-between">
             <HStack className="flex-1 items-center" space="md">
               {renderDeviceIcon(device)}
               <VStack className="flex-1">
                 <Text className={`font-medium ${isSelected ? 'text-blue-900 dark:text-blue-100' : 'text-gray-900 dark:text-gray-100'}`}>{device.name}</Text>
                 <Text className={`text-sm ${isSelected ? 'text-blue-700 dark:text-blue-300' : 'text-gray-500 dark:text-gray-400'}`}>
                   {deviceTypeLabel}
                   {unavailableText}
                 </Text>
               </VStack>
             </HStack>
-            {isSelected && <CheckCircle size={20} className="text-blue-600 dark:text-blue-400" />}
+            {isSelected ? <CheckCircle size={20} className="text-blue-600 dark:text-blue-400" /> : null}
           </HStack>
         </Card>
       </Pressable>

22-32: Confirm className support on lucide-react-native icons

On React Native, lucide icons typically accept color/size props; className styling depends on your NativeWind/tailwind setup. If className isn’t applied to SVGs in your config, these icons won’t get the intended colors.

If needed, switch to explicit color props:

- return <Headphones size={20} className="text-gray-600 dark:text-gray-400" />;
+ return <Headphones size={20} color={isDarkMode ? '#9CA3AF' : '#4B5563'} />;

I can check your NativeWind config and provide a safe theme-aware helper for icon colors.


114-116: Avoid inline arrow functions in render for onPress handlers

Inline lambdas within map cause re-creation each render. Not critical here, but you can stabilize handlers with useCallback and pass the device id.

For example:

const onSelectMic = React.useCallback((device: AudioDeviceInfo) => () => setSelectedMicrophone(device), [setSelectedMicrophone]);
const onSelectSpk = React.useCallback((device: AudioDeviceInfo) => () => setSelectedSpeaker(device), [setSelectedSpeaker]);

// ...
availableMicrophones.map((device) => renderDeviceItem(device, selectedAudioDevices.microphone?.id === device.id, onSelectMic(device), 'microphone'))
availableSpeakers.map((device) => renderDeviceItem(device, selectedAudioDevices.speaker?.id === device.id, onSelectSpk(device), 'speaker'))

Also applies to: 132-134

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e8c9b7f and 3e68e0d.

📒 Files selected for processing (7)
  • src/components/livekit/livekit-bottom-sheet.tsx (3 hunks)
  • src/components/settings/__tests__/audio-device-selection.test.tsx (1 hunks)
  • src/components/settings/audio-device-selection.tsx (3 hunks)
  • src/stores/app/bluetooth-audio-store.ts (1 hunks)
  • src/translations/ar.json (1 hunks)
  • src/translations/en.json (1 hunks)
  • src/translations/es.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names (e.g., isFetchingData, handleUserInput)
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Use Expo SecureStore for sensitive data
Use zustand for state management
Use react-query for data fetching and caching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Use React Navigation for handling navigation and deep linking
Handle errors gracefully and provide user feedback
Use Expo's SecureStore for sensitive data
Use zustand for state management
Use react-query for data fetching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

Files:

  • src/stores/app/bluetooth-audio-store.ts
  • src/components/livekit/livekit-bottom-sheet.tsx
  • src/components/settings/audio-device-selection.tsx
  • src/components/settings/__tests__/audio-device-selection.test.tsx
**/*

📄 CodeRabbit Inference Engine (.cursorrules)

Directory and file names should be lowercase and hyphenated (e.g., user-profile, chat-screen)

Files:

  • src/stores/app/bluetooth-audio-store.ts
  • src/translations/ar.json
  • src/components/livekit/livekit-bottom-sheet.tsx
  • src/components/settings/audio-device-selection.tsx
  • src/translations/es.json
  • src/components/settings/__tests__/audio-device-selection.test.tsx
  • src/translations/en.json
src/translations/**

📄 CodeRabbit Inference Engine (.cursorrules)

Store translation dictionaries in src/translations

Files:

  • src/translations/ar.json
  • src/translations/es.json
  • src/translations/en.json
src/translations/**/*.{json,ts}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Store translation dictionaries in src/translations

Files:

  • src/translations/ar.json
  • src/translations/es.json
  • src/translations/en.json
**/*.tsx

📄 CodeRabbit Inference Engine (.cursorrules)

**/*.tsx: Use functional components and hooks over class components
Use PascalCase for React component names (e.g., UserProfile, ChatScreen)
Utilize React.FC for defining functional components with props
Minimize useEffect, useState, and heavy computations inside render
Use React.memo() for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, windowSize
Use getItemLayout for FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers to prevent re-renders
Use gluestack-ui for styling; if no component exists in src/components/ui, style via StyleSheet.create or styled-components
Optimize images using react-native-fast-image
Use React Navigation for navigation and deep linking with best practices
Wrap all user-facing text in t() from react-i18next
Use react-hook-form for form handling
Use @rnmapbox/maps for maps or vehicle navigation
Use lucide-react-native for icons directly in markup; do not use the gluestack-ui icon component
Use the conditional operator (?:) for conditional rendering instead of &&

**/*.tsx: Use functional components and hooks over class components
Use PascalCase for component names
Utilize React.FC for defining functional components with props
Minimize useEffect/useState and heavy computations inside render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Use getItemLayout for FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers
Optimize image handling using react-native-fast-image
Wrap all user-facing text in t() from react-i18next
Ensure support for dark mode and light mode
Ensure the app is accessible following WCAG for mobile
Use react-hook-form for form handling
Use @rnmapbox/maps for maps and navigation
Use lucide-react-native for icons directly in markup; do not us...

Files:

  • src/components/livekit/livekit-bottom-sheet.tsx
  • src/components/settings/audio-device-selection.tsx
  • src/components/settings/__tests__/audio-device-selection.test.tsx
{components/ui/**/*.{ts,tsx},**/*.tsx}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Use gluestack-ui consistently; if no component exists in components/ui, style via StyleSheet.create or styled-components

Files:

  • src/components/livekit/livekit-bottom-sheet.tsx
  • src/components/settings/audio-device-selection.tsx
  • src/components/settings/__tests__/audio-device-selection.test.tsx
**/*.{test.ts,test.tsx,spec.ts,spec.tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

Create and use Jest tests to validate all generated components

Files:

  • src/components/settings/__tests__/audio-device-selection.test.tsx
**/__tests__/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

Generate tests for all components, services, and logic; ensure tests run without errors

Files:

  • src/components/settings/__tests__/audio-device-selection.test.tsx
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Create and use Jest tests for all generated components

Files:

  • src/components/settings/__tests__/audio-device-selection.test.tsx
🧠 Learnings (4)
📚 Learning: 2025-08-12T03:33:40.227Z
Learnt from: CR
PR: Resgrid/Unit#0
File: .cursorrules:0-0
Timestamp: 2025-08-12T03:33:40.227Z
Learning: Applies to **/*.{ts,tsx} : Use react-i18next for internationalization

Applied to files:

  • src/components/settings/audio-device-selection.tsx
📚 Learning: 2025-08-12T03:33:40.227Z
Learnt from: CR
PR: Resgrid/Unit#0
File: .cursorrules:0-0
Timestamp: 2025-08-12T03:33:40.227Z
Learning: Applies to **/*.tsx : Wrap all user-facing text in t() from react-i18next

Applied to files:

  • src/components/settings/audio-device-selection.tsx
📚 Learning: 2025-08-12T03:33:40.227Z
Learnt from: CR
PR: Resgrid/Unit#0
File: .cursorrules:0-0
Timestamp: 2025-08-12T03:33:40.227Z
Learning: Applies to **/*.{test.ts,test.tsx,spec.ts,spec.tsx} : Create and use Jest tests to validate all generated components

Applied to files:

  • src/components/settings/__tests__/audio-device-selection.test.tsx
📚 Learning: 2025-08-12T03:34:25.963Z
Learnt from: CR
PR: Resgrid/Unit#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-12T03:34:25.963Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Create and use Jest tests for all generated components

Applied to files:

  • src/components/settings/__tests__/audio-device-selection.test.tsx
🧬 Code Graph Analysis (2)
src/components/settings/audio-device-selection.tsx (1)
src/stores/app/bluetooth-audio-store.ts (2)
  • useBluetoothAudioStore (105-228)
  • AudioDeviceInfo (38-43)
src/components/settings/__tests__/audio-device-selection.test.tsx (2)
src/stores/app/bluetooth-audio-store.ts (2)
  • AudioDeviceInfo (38-43)
  • AudioDeviceSelection (45-48)
src/components/settings/audio-device-selection.tsx (1)
  • AudioDeviceSelection (18-143)
🔇 Additional comments (6)
src/stores/app/bluetooth-audio-store.ts (1)

114-116: No lingering ‘default’ speaker type checks; change is safe
I searched for any selectedAudioDevices.speaker?.type === 'default' comparisons and fixtures/mocks using a default-speaker with type: 'default' and found none. The switch to type: 'speaker' in both availableAudioDevices (lines 114–116) and selectedAudioDevices (lines 118–120) is safe.

Optional refactor: centralize your defaults as constants in src/stores/app/bluetooth-audio-store.ts for consistency and to avoid drift:

const DEFAULT_MIC: AudioDeviceInfo = {
  id: 'default-mic',
  name: 'Default Microphone',
  type: 'default',
  isAvailable: true,
};
const DEFAULT_SPEAKER: AudioDeviceInfo = {
  id: 'default-speaker',
  name: 'Default Speaker',
  type: 'speaker',
  isAvailable: true,
};

Then reuse DEFAULT_MIC/DEFAULT_SPEAKER in both availableAudioDevices and selectedAudioDevices.

src/translations/en.json (1)

534-546: Good addition: complete audio device selection keys present and consistent

Keys are organized under settings.audio_device_selection, consistent with other locales and referenced UI. No issues found.

src/translations/es.json (1)

534-546: Spanish translations added correctly and align with the English keys

The 11 keys match the English set and read naturally. Looks good.

src/translations/ar.json (1)

534-546: Arabic translations added correctly and mirror other locales

The keys are complete and appropriate for RTL contexts. Approved.

src/components/settings/__tests__/audio-device-selection.test.tsx (1)

45-47: Mocking the store looks good and keeps tests deterministic

The mock store shape matches the interface used by the component and isolates tests from Zustand. Good call.

src/components/settings/audio-device-selection.tsx (1)

73-76: Verify filtering semantics: speaker devices appear under “Microphone”

current logic shows all non-bluetooth devices (including type 'speaker') in the Microphone section, and only available devices in the Speaker section. If the intent was to show device “roles” rather than connection “types”, this may be confusing to users.

Would you like me to adjust filtering to separate input vs output devices (if the store exposes that) or at least hide speaker-type devices from the microphone list?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/services/callkeep.service.ios.ts (1)

57-61: Bug: Pass numeric options to RNCallKeep instead of strings

RNCallKeep iOS options expect numbers for maximumCallGroups and maximumCallsPerCallGroup. Converting to strings can cause native type mismatches at runtime.

Apply this diff:

       const options = {
         ios: {
           appName: config.appName,
-          maximumCallGroups: config.maximumCallGroups.toString(),
-          maximumCallsPerCallGroup: config.maximumCallsPerCallGroup.toString(),
+          maximumCallGroups: config.maximumCallGroups,
+          maximumCallsPerCallGroup: config.maximumCallsPerCallGroup,
           includesCallsInRecents: config.includesCallsInRecents,
           supportsVideo: config.supportsVideo,
           ringtoneSound: config.ringtoneSound,
         },
src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (1)

527-548: Ensure env var cleanup even on test failure (use try/finally)

If this test throws before restoration, STORYBOOK_LIVEKIT_TOKEN may leak into other tests. Wrap with try/finally to guarantee cleanup.

-    it('should handle token fetch errors', async () => {
-      // Mock environment to simulate missing token
-      const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
-      delete process.env.STORYBOOK_LIVEKIT_TOKEN;
-      
-      const { result } = renderHook(() => useLiveKitCallStore());
-
-      await act(async () => {
-        await result.current.actions.connectToRoom('test-room', 'test-participant');
-      });
-
-      expect(result.current.error).toBe('Failed to fetch a valid connection token.');
-      expect(result.current.isConnecting).toBe(false);
-      expect(result.current.isConnected).toBe(false);
-      expect(result.current.roomInstance).toBeNull();
-      expect(result.current.currentRoomId).toBeNull();
-      
-      // Restore original environment
-      if (originalEnv) {
-        process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
-      }
-    });
+    it('should handle token fetch errors', async () => {
+      // Mock environment to simulate missing token
+      const originalEnv = process.env.STORYBOOK_LIVEKIT_TOKEN;
+      try {
+        delete process.env.STORYBOOK_LIVEKIT_TOKEN;
+        const { result } = renderHook(() => useLiveKitCallStore());
+        await act(async () => {
+          await result.current.actions.connectToRoom('test-room', 'test-participant');
+        });
+        expect(result.current.error).toBe('Failed to fetch a valid connection token.');
+        expect(result.current.isConnecting).toBe(false);
+        expect(result.current.isConnected).toBe(false);
+        expect(result.current.roomInstance).toBeNull();
+        expect(result.current.currentRoomId).toBeNull();
+      } finally {
+        if (originalEnv) {
+          process.env.STORYBOOK_LIVEKIT_TOKEN = originalEnv;
+        } else {
+          delete process.env.STORYBOOK_LIVEKIT_TOKEN;
+        }
+      }
+    });
🧹 Nitpick comments (6)
src/features/livekit-call/store/useLiveKitCallStore.ts (3)

6-6: Decouple from iOS-specific service path to enable platform resolution

Importing the .ios file directly ties this store to iOS at build time. Prefer importing from a platform-agnostic module so Metro can resolve the correct platform file (ios/android/native) and you can provide a no-op or Android implementation later without touching callers.

-import { callKeepService } from '../../../services/callkeep.service.ios';
+// Let RN resolve the platform-specific service (provide .ios/.android/.native files)
+import { callKeepService } from '../../../services/callkeep.service';

If you want, I can scaffold a no-op Android variant to keep the API consistent.


141-176: Start CallKeep on connect: looks good; also guard against unhandled mic/camera promise rejections

The CallKeep start flow is clean and well-logged. One nit: setMicrophoneEnabled/setCameraEnabled return Promises; if they reject, they can raise unhandled rejections since they aren’t awaited or caught here.

You can defensively attach catch handlers without blocking the connect path:

// immediately after setting connection state, before/around CallKeep start
void newRoom.localParticipant.setMicrophoneEnabled(true).catch((e) => {
  logger.warn({ message: 'Failed to enable microphone after connect', context: { error: e, roomId } });
});
void newRoom.localParticipant.setCameraEnabled(false).catch((e) => {
  logger.debug({ message: 'Failed to disable camera after connect (video unsupported)', context: { error: e, roomId } });
});

318-331: Avoid duplicating CallKeep end across multiple paths

You end the CallKeep call here and also in the ConnectionStateChanged → Disconnected handler. While harmless (the service no-ops if there’s no active call), consider consolidating to a single place to reduce duplicate calls and logs.

src/services/callkeep.service.ios.ts (1)

38-45: Clarify platform intent (Android options present but never used)

setup() returns early for non-iOS, so Android options are currently dead code. Either:

  • support Android here (remove the early return and let RNCallKeep.setup run cross-platform with appropriate options), or
  • remove the Android options to avoid confusion and keep this service strictly iOS.

Given the filename is .ios.ts, the latter is consistent; otherwise consider creating platform-specific files with a shared import path.

Also applies to: 64-70

src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (2)

463-471: Fix misleading test comment about connection prevention

The test sets isConnecting to true, so the first attempt is prevented by design. Update the comment to reflect the behavior under test.

-      // First connection attempt should succeed
+      // First connection attempt should be prevented due to isConnecting = true

139-140: Prefer spying on Platform.OS getter over property re-assignment

Directly mutating Platform.OS can be brittle. Using a getter spy avoids tampering with a potentially read-only property across RN versions.

Example:

const osSpy = jest.spyOn(Platform, 'OS', 'get');
osSpy.mockReturnValue('android');
// ...
osSpy.mockRestore();

Also applies to: 236-237

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3e68e0d and 66fc08b.

📒 Files selected for processing (5)
  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (1 hunks)
  • src/features/livekit-call/store/useLiveKitCallStore.ts (11 hunks)
  • src/services/__tests__/callkeep.service.ios.test.ts (1 hunks)
  • src/services/callkeep.service.ios.ts (1 hunks)
  • src/stores/app/__tests__/livekit-store.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/stores/app/tests/livekit-store.test.ts
  • src/services/tests/callkeep.service.ios.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names (e.g., isFetchingData, handleUserInput)
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Use Expo SecureStore for sensitive data
Use zustand for state management
Use react-query for data fetching and caching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components, favoring interfaces for props and state
Avoid using any; strive for precise types
Use React Navigation for handling navigation and deep linking
Handle errors gracefully and provide user feedback
Use Expo's SecureStore for sensitive data
Use zustand for state management
Use react-query for data fetching
Use react-i18next for internationalization
Use react-native-mmkv for local storage
Use axios for API requests

Files:

  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
  • src/services/callkeep.service.ios.ts
  • src/features/livekit-call/store/useLiveKitCallStore.ts
**/*

📄 CodeRabbit Inference Engine (.cursorrules)

Directory and file names should be lowercase and hyphenated (e.g., user-profile, chat-screen)

Files:

  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
  • src/services/callkeep.service.ios.ts
  • src/features/livekit-call/store/useLiveKitCallStore.ts
**/*.{test.ts,test.tsx,spec.ts,spec.tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

Create and use Jest tests to validate all generated components

Files:

  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
**/__tests__/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursorrules)

Generate tests for all components, services, and logic; ensure tests run without errors

Files:

  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit Inference Engine (.github/copilot-instructions.md)

Create and use Jest tests for all generated components

Files:

  • src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts
🧬 Code Graph Analysis (3)
src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (2)
src/lib/logging/index.tsx (2)
  • logger (80-80)
  • error (74-76)
src/features/livekit-call/store/useLiveKitCallStore.ts (1)
  • useLiveKitCallStore (81-385)
src/services/callkeep.service.ios.ts (1)
src/lib/logging/index.tsx (2)
  • logger (80-80)
  • error (74-76)
src/features/livekit-call/store/useLiveKitCallStore.ts (3)
src/lib/logging/index.tsx (2)
  • logger (80-80)
  • error (74-76)
__mocks__/livekit-client.ts (4)
  • Room (1-12)
  • ConnectionState (25-30)
  • RoomEvent (14-23)
  • RemoteParticipant (32-32)
src/services/callkeep.service.ios.ts (1)
  • callKeepService (317-317)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (7)
src/features/livekit-call/store/useLiveKitCallStore.ts (4)

93-114: LGTM: iOS CallKeep setup is correctly gated and error-mapped

Good platform gating, structured logging, and user-facing error mapping on setup failures. The numeric constraints look reasonable for single-call usage.


188-204: LGTM: Ending CallKeep on disconnect

Cleanly ends the CallKeep call on iOS with appropriate logging on both success and failure paths.


14-17: LGTM: Improved observability for token fetch and misconfiguration

The structured debug/warn logging around token fetching and placeholder URL is helpful for diagnosing connection issues in dev/test.

Also applies to: 26-34


120-123: LGTM: Safe-guard against duplicate connection attempts

The early-exit with a clear warn log prevents racey multi-connects and keeps the state machine simpler.

src/services/callkeep.service.ios.ts (2)

114-147: LGTM: Start call lifecycle and audio-session activation

The startCall/reportConnecting/reportConnected sequence and audio-session event handling are consistent with CallKit patterns. Good defensive cleanup of existing calls before starting a new one.


221-227: Confirmed bulk removal of RNCallKeep event listeners

Verified against react-native-callkeep v4.x documentation:
• Calling RNCallKeep.removeEventListener('eventName') without a callback removes all listeners for that event.
• Supplying both the event name and callback removes only that specific listener.

Your current pattern—adding anonymous callbacks and later calling removeEventListener('…')—will correctly clean up all handlers for those events. No changes are required here.

If you ever need to remove only a single listener, store the callback reference and use:

RNCallKeep.removeEventListener('eventName', yourCallback);
src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (1)

121-136: Solid coverage and realistic integration flow

Good use of mocks, platform gating scenarios, and lifecycle assertions (connect, disconnect, start/end CallKeep, mic control, prevention, and error paths). The structured logger assertions align with the store’s behavior.

Also applies to: 175-186, 217-233, 288-347, 387-451, 455-506, 510-525

@ucswift
Copy link
Copy Markdown
Member Author

ucswift commented Aug 16, 2025

Approve

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit 2d204d0 into master Aug 16, 2025
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant