Skip to content

RIC-T40 Fixes - #22

Merged
ucswift merged 2 commits into
masterfrom
develop
Jul 25, 2026
Merged

RIC-T40 Fixes#22
ucswift merged 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Jul 25, 2026

Copy link
Copy Markdown
Member

PR Description

Overview

This PR implements RIC-T40 Fixes, a collection of stability, robustness, and feature improvements across the Resgrid IC mobile application.

Key Changes

🔐 Authentication Stability

  • Single-flight token refresh: Introduced refreshTokenSingleFlight wrapper to ensure concurrent refresh requests (from the axios 401 interceptor and proactive refresh timer) share a single in-flight request, preventing the rotating refresh token from being consumed multiple times.
  • Safe expires_in handling: Added fallback to 3600 seconds when expires_in is missing or undefined, preventing crashes during login, SSO login, and token refresh.
  • API timeouts: Added 15-second timeouts to the main API client and auth API instances.

🗺️ Maps Route Restructuring

  • Moved Maps from a top-level unauthenticated route into the authenticated app shell as a hidden tab route, preserving the app header and tab bar across nested map screens.
  • Added maps-route.ts helper to dynamically set the correct header (drawer menu on landing, back button on child routes) and title based on the current pathname.
  • Created _layout.tsx for Maps with a Stack navigator supporting index, search, custom maps, and indoor maps screens.

🔍 Incidents Search

  • Added a search bar to the incidents screen allowing filtering by CallId, CallNumber, Name, CallName, CallAddress, CommanderName, and CommandPostLocation.
  • Shows a search-specific empty state when no results match, with a clear button to reset.

📍 Command Map Camera

  • Implemented priority-based imperative camera recentering (saved view > ICP > first pin) to handle asynchronous pin/ICP arrival after mount.
  • Added camera ref and auto-center priority tracking to avoid unnecessary recentering once a higher-priority target has been applied.

🛡️ Robustness & Null-Safety Fixes

  • Map markers: Added null coalescing (MapMakerInfos ?? []) in map rendering and SignalR update handler.
  • Cache manager: Graceful handling of corrupted cache entries (catches JSON parse errors, deletes the bad entry, returns null).
  • Call detail: Validates dates with isValid before formatting, falling back to raw string or empty value.
  • Contact notes: Safer date sorting that handles invalid dates without throwing.
  • Units & Dispatch stores: Null-safe handling of API response data arrays.

📡 SignalR Improvements

  • Access token factory now reads lazily from the auth store, ensuring automatic reconnects use the current valid token.
  • Pending reconnect timers are now cancelled on disconnect, preventing stale reconnection attempts after teardown.
  • Event listener callbacks are wrapped in try-catch to prevent unhandled errors from propagating.

📱 Push Notifications

  • Moved notifee.onBackgroundEvent registration to module scope so notification action presses work when the app is killed (headless JS task).
  • Made the check-in action handler resilient — it never throws, preventing unhandled promise rejections that could crash the background task.

📞 LiveKit

  • Added Reconnecting and Disconnected event handlers with automatic cleanup on unexpected disconnects.
  • Made disconnectFromRoom resilient — cleanup continues even if the disconnect call or sound playback fails.

📍 Location Service

  • Added background permission re-check before starting background updates.
  • Wrapped app state change handler in try-catch to prevent unhandled rejections.

🔧 Other

  • Bluetooth audio: Awaiting scan failures so they're caught in the error handler.
  • CallKeep service: Refactored to a TypeScript barrel file that delegates to platform-specific implementations resolved by Metro/Jest.
  • LiveKit call store: Fails fast when no token endpoint is available instead of leaving isConnecting in a permanent state.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@ucswift, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a764d190-d4c4-4543-94db-3bebd24461d9

📥 Commits

Reviewing files that changed from the base of the PR and between df2f68f and 63b2c4f.

📒 Files selected for processing (48)
  • scripts/__tests__/extract-release-notes.test.ts
  • scripts/extract-release-notes.sh
  • src/api/common/client.tsx
  • src/app/(app)/__tests__/incidents.test.tsx
  • src/app/(app)/_layout.tsx
  • src/app/(app)/incidents.tsx
  • src/app/(app)/index.tsx
  • src/app/(app)/maps/_layout.tsx
  • src/app/(app)/maps/custom/[id].tsx
  • src/app/(app)/maps/index.tsx
  • src/app/(app)/maps/indoor/[id].tsx
  • src/app/(app)/maps/search.tsx
  • src/app/_layout.tsx
  • src/app/call/[id].tsx
  • src/app/call/__tests__/[id].security.test.tsx
  • src/app/call/__tests__/[id].test.tsx
  • src/app/command-map/[callId].tsx
  • src/app/maps/_layout.tsx
  • src/components/contacts/contact-notes-list.tsx
  • src/features/livekit-call/store/useLiveKitCallStore.ts
  • src/hooks/use-map-signalr-updates.ts
  • src/lib/__tests__/maps-route.test.ts
  • src/lib/auth/api.tsx
  • src/lib/cache/cache-manager.ts
  • src/lib/maps-route.ts
  • src/lib/storage/app.tsx
  • src/lib/storage/index.tsx
  • src/services/__tests__/location.test.ts
  • src/services/__tests__/push-notification.test.ts
  • src/services/__tests__/signalr.service.enhanced.test.ts
  • src/services/bluetooth-audio.service.ts
  • src/services/callkeep.service.ts
  • src/services/location.ts
  • src/services/push-notification.ts
  • src/services/signalr.service.ts
  • src/stores/app/livekit-store.ts
  • src/stores/auth/store.tsx
  • src/stores/dispatch/store.ts
  • src/stores/units/store.ts
  • src/translations/ar.json
  • src/translations/de.json
  • src/translations/en.json
  • src/translations/es.json
  • src/translations/fr.json
  • src/translations/it.json
  • src/translations/pl.json
  • src/translations/sv.json
  • src/translations/uk.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Resgrid-Bot

This comment has been minimized.

Comment thread src/api/common/client.tsx
// Create axios instance with default config
const axiosInstance: AxiosInstance = axios.create({
baseURL: getBaseApiUrl(),
timeout: 15000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

The magic number 15000 used for the axios timeout obscures intent and complicates future tuning. Extract a shared constant such as DEFAULT_REQUEST_TIMEOUT_MS = 15000 to allow consistent reuse across the codebase.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/api/common/client.tsx:

Line 11:

The magic number `15000` used for the axios timeout obscures intent and complicates future tuning. Extract a shared constant such as `DEFAULT_REQUEST_TIMEOUT_MS = 15000` to allow consistent reuse across the codebase.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

</InputSlot>
<InputField testID="incidents-search" placeholder={t('incidents.search')} value={searchQuery} onChangeText={setSearchQuery} returnKeyType="search" />
{searchQuery ? (
<Pressable className="h-full items-center justify-center pr-3" onPress={() => setSearchQuery('')} accessibilityLabel={t('common.clear')} accessibilityRole="button" testID="incidents-search-clear" hitSlop={8}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Inline arrow functions in JSX props create new functions on every render, causing unnecessary performance overhead. Move the function definition outside the render method to comply with the 'Avoid using .bind() or arrow functions in JSX props' rule.

Prompt for LLM

File src/app/(app)/incidents.tsx:

Line 93:

Inline arrow functions in JSX props create new functions on every render, causing unnecessary performance overhead. Move the function definition outside the render method to comply with the 'Avoid using .bind() or arrow functions in JSX props' rule.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

import { useMapsStore } from '@/stores/maps/store';

export default function CustomMapViewer() {
export default function CustomMapViewer(): React.JSX.Element {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

The CustomMapViewer component uses a default export, reducing clarity and maintainability during refactoring. Change to a named export like export function CustomMapViewer(): React.JSX.Element to improve autocomplete and rename operations.

Kody rule violation: Avoid default exports

Prompt for LLM

File src/app/(app)/maps/custom/[id].tsx:

Line 20:

The `CustomMapViewer` component uses a default export, reducing clarity and maintainability during refactoring. Change to a named export like `export function CustomMapViewer(): React.JSX.Element` to improve autocomplete and rename operations.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

return;
}

const first = incidentPins[0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Direct array indexing without a bounds check risks returning undefined on empty arrays. Add an explicit length guard, such as const first = incidentPins.length > 0 ? incidentPins[0] : null, before dereferencing incidentPins.

Kody rule violation: Check query results before accessing indices

Prompt for LLM

File src/app/command-map/[callId].tsx:

Line 150:

Direct array indexing without a bounds check risks returning undefined on empty arrays. Add an explicit length guard, such as `const first = incidentPins.length > 0 ? incidentPins[0] : null`, before dereferencing `incidentPins`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/lib/auth/api.tsx
*/
let inFlightRefresh: Promise<AuthResponse> | null = null;

export const refreshTokenSingleFlight = (refreshToken: string): Promise<AuthResponse> => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

The JSDoc block for the refreshTokenSingleFlight function (lines 92-97) lacks a @returns {Promise<AuthResponse>} tag and rejection condition documentation. Move the JSDoc directly above the function declaration and add the missing @returns and rejection details.

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

File src/lib/auth/api.tsx:

Line 100:

The JSDoc block for the `refreshTokenSingleFlight` function (lines 92-97) lacks a `@returns {Promise<AuthResponse>}` tag and rejection condition documentation. Move the JSDoc directly above the function declaration and add the missing `@returns` and rejection details.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

let cacheItem: CacheItem<T>;
try {
cacheItem = JSON.parse(cached);
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

The catch block silently discards JSON parse failures without logging the exception, making cache corruption impossible to diagnose. Capture the error and log it with structured context using logger.error('cache parse failed', { key, err }) before returning null.

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File src/lib/cache/cache-manager.ts:

Line 52:

The catch block silently discards JSON parse failures without logging the exception, making cache corruption impossible to diagnose. Capture the error and log it with structured context using `logger.error('cache parse failed', { key, err })` before returning null.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/lib/maps-route.ts
export const getMapsHeaderState = (pathname: string): MapsHeaderState => {
const normalizedPathname = pathname.replace(/\/+$/, '') || '/';

if (normalizedPathname === '/maps') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

The inline string literal '/maps' bypasses route centralization requirements. Extract the path into a named constant such as const MAPS_BASE_ROUTE = '/maps' to prevent typos and improve maintainability.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/lib/maps-route.ts:

Line 11:

The inline string literal `'/maps'` bypasses route centralization requirements. Extract the path into a named constant such as `const MAPS_BASE_ROUTE = '/maps'` to prevent typos and improve maintainability.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/lib/maps-route.ts
@@ -0,0 +1,43 @@
export type MapsHeaderTitleKey = 'maps.custom_maps' | 'maps.indoor_maps' | 'maps.search_maps' | 'maps.title';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

The hand-enumerated MapsHeaderTitleKey type risks diverging from runtime string values. Derive the type from a const tuple using const MAPS_HEADER_TITLE_KEYS = [...] as const and typeof MAPS_HEADER_TITLE_KEYS[number].

Kody rule violation: Derive TypeScript types from validation schemas

Prompt for LLM

File src/lib/maps-route.ts:

Line 1:

The hand-enumerated `MapsHeaderTitleKey` type risks diverging from runtime string values. Derive the type from a const tuple using `const MAPS_HEADER_TITLE_KEYS = [...] as const` and `typeof MAPS_HEADER_TITLE_KEYS[number]`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/services/location.ts Outdated
},
});
useLocationStore.getState().setLocation(location);
sendLocationToAPI(location); // Send to API for background updates

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

The fire-and-forget sendLocationToAPI(location) call inside the watchPosition callback leaves Promise rejections unhandled. Append a .catch() handler or wrap an await call in try/catch to properly capture and log network errors.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/services/location.ts:

Line 286:

The fire-and-forget `sendLocationToAPI(location)` call inside the `watchPosition` callback leaves Promise rejections unhandled. Append a `.catch()` handler or wrap an `await` call in `try/catch` to properly capture and log network errors.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

// never fire for action presses from the killed state.
if (Platform.OS !== 'web') {
notifee.onBackgroundEvent(async ({ type, detail }) => {
if (type === EventType.ACTION_PRESS && detail.pressAction?.id === 'check-in') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

The raw string literal 'check-in' used for the press-action identifier is inconsistent and fragile. Define a shared constant like const PRESS_ACTION_CHECK_IN = 'check-in' or an enum to align with existing centralized identifiers.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/services/push-notification.ts:

Line 443:

The raw string literal `'check-in'` used for the press-action identifier is inconsistent and fragile. Define a shared constant like `const PRESS_ACTION_CHECK_IN = 'check-in'` or an enum to align with existing centralized identifiers.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +646 to +649
logger.error({
message: `Error in SignalR event listener for event: ${event}`,
context: { error },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

The error log embeds the event name inside the message string instead of as a structured field. Add the event and hub as explicit structured fields like op: 'signalr.emit', event to ensure logs remain searchable and filterable.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File src/services/signalr.service.ts:

Line 646 to 649:

The error log embeds the event name inside the message string instead of as a structured field. Add the event and hub as explicit structured fields like `op: 'signalr.emit', event` to ensure logs remain searchable and filterable.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines +575 to +589
room.on(RoomEvent.Disconnected, () => {
logger.warn({
message: 'LiveKit room disconnected (server kick or unrecoverable network loss)',
context: { roomName: roomInfo.Name },
});
// Only clean up if this room is still the active one (avoids racing a manual disconnect)
if (get().currentRoom === room) {
Promise.resolve(get().disconnectFromRoom()).catch((cleanupError: unknown) => {
logger.error({
message: 'Failed to clean up after LiveKit room disconnect',
context: { error: cleanupError },
});
});
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug high

The RoomEvent.Disconnected handler (line 575-589) fires re-entrant cleanup during a manual disconnect because currentRoom is only set to null at the end of disconnectFromRoom() (line 850). Clear currentRoom or set an isDisconnecting flag before calling currentRoom.disconnect() (line 790) to prevent concurrent async cleanup chains and duplicated sounds.

// In disconnectFromRoom(), clear currentRoom before disconnecting:
//   const { currentRoom } = get();
//   if (currentRoom) {
//     set({ currentRoom: null });  // clear FIRST so the Disconnected handler's guard skips re-entrant cleanup
//     try { await currentRoom.disconnect(); } catch ...
// ... rest of cleanup references the local `currentRoom` variable
Prompt for LLM

File src/stores/app/livekit-store.ts:

Line 575 to 589:

The `RoomEvent.Disconnected` handler (line 575-589) fires re-entrant cleanup during a manual disconnect because `currentRoom` is only set to null at the end of `disconnectFromRoom()` (line 850). Clear `currentRoom` or set an `isDisconnecting` flag before calling `currentRoom.disconnect()` (line 790) to prevent concurrent async cleanup chains and duplicated sounds.

Suggested Code:

// In disconnectFromRoom(), clear currentRoom before disconnecting:
//   const { currentRoom } = get();
//   if (currentRoom) {
//     set({ currentRoom: null });  // clear FIRST so the Disconnected handler's guard skips re-entrant cleanup
//     try { await currentRoom.disconnect(); } catch ...
// ... rest of cleanup references the local `currentRoom` variable

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

Resgrid-Bot commented Jul 25, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@ucswift
ucswift merged commit 5f95be3 into master Jul 25, 2026
9 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.

2 participants