Skip to content

CU-868ex18rd Working on geolocation issue minor fixes.#67

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

CU-868ex18rd Working on geolocation issue minor fixes.#67
ucswift merged 3 commits intomasterfrom
develop

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Aug 29, 2025

Summary by CodeRabbit

  • New Features
    • App-level location tracking lifecycle (starts/stops at app level, stops on sign-out); realtime geolocation setting with background/foreground gating.
    • Login: "Change Server URL" button + bottom sheet, analytics event, and translations (en/es/ar).
  • Bug Fixes
    • Removed per-screen map location lifecycle and toasts; foreground/background API calls gated by realtime setting.
    • Bluetooth permission/request timing improved and reduced Android permission set.
  • Style
    • Notification badge styling, overflow capped at "99+" and improved accessibility labels.
  • Tests
    • Added/expanded tests for login, notifications, realtime geolocation, Bluetooth audio, and location services.

@coderabbitai
Copy link

coderabbitai bot commented Aug 29, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

App-level location lifecycle moved to _layout (start/stop guarded); per-screen tracking removed. Realtime geolocation support added (storage gating, updater registration, API send gating). Bluetooth audio init now permission-gated with a 500ms delay. Login form adds Server URL flow and analytics; notifications badge capping and accessibility added. Tests and translations updated.

Changes

Cohort / File(s) Summary
App layout & map
src/app/(app)/_layout.tsx, src/app/(app)/map.tsx
Moved location start to app init (guarded after offlineQueueService.initialize()), stop on sign-out; removed per-screen start/stop in map; added logs and guarded error handling for locationService.{start,stop} calls.
Login UI & tests
src/app/login/login-form.tsx, src/app/login/__tests__/login-form.test.tsx, src/translations/*.json
Added Server URL change button, ServerUrlBottomSheet integration, analytics trackEvent on press, i18n keys and tests; replaced static strings with translation keys.
Notifications UI & tests
src/components/notifications/NotificationButton.tsx, src/components/notifications/__tests__/NotificationButton.test.tsx
Added i18n, accessibilityRole/Label, unread count tracking, cap badge at "99+", style adjustments; comprehensive tests for loading, counts, overflow, accessibility.
Realtime geolocation hook & tests
src/lib/hooks/use-realtime-geolocation.ts, src/lib/hooks/__tests__/use-realtime-geolocation.test.ts
Replaced static storage key with getRealtimeGeolocationStorageKey(); added save/load helpers; introduced registerLocationServiceRealtimeUpdater and module updater invocation when toggling; tests for enable/disable, hub connect/disconnect, logging and error paths.
Location service & tests
src/services/location.ts, src/services/__tests__/location.test.ts
Added isRealtimeGeolocationEnabled state and updateRealtimeGeolocationSetting(); made sendLocationToAPI accept realtime flag and export it; foreground updates update local store only; background task reads persisted realtime flag and sends to API when enabled; permission and background handling refactored; tests expanded.
Bluetooth audio service & tests
src/services/bluetooth-audio.service.ts, src/services/__tests__/bluetooth-audio.service.test.ts
Added 500ms startup delay before permission requests; reduced Android perms required; initialize BleManager only after permissions granted; tests validate delay, platform branches, denial/failure cases.
Misc tests & translations
src/lib/hooks/__tests__/*, src/services/__tests__/*, src/translations/*
Added/updated tests for realtime geolocation, location service, bluetooth permissions, notifications; added translation keys (login.change_server_url, notifications_badge_overflow, notifications_button) across locales.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as App (_layout)
  participant Offline as offlineQueueService
  participant Loc as locationService
  participant Auth as AuthState

  App->>Offline: initialize()
  Offline-->>App: resolved
  App->>Loc: startLocationUpdates() (guarded)
  alt started
    Loc-->>App: started (log)
  else error
    Loc-->>App: error (logged, init continues)
  end

  Auth-->>App: status -> signedOut
  App->>Loc: stopLocationUpdates() (guarded)
  Loc-->>App: stopped or error (logged)
Loading
sequenceDiagram
  autonumber
  participant UI as LoginForm
  participant Analytics as useAnalytics
  participant Sheet as ServerUrlBottomSheet

  UI->>Analytics: trackEvent('login_server_url_pressed')
  Analytics-->>UI: ack / error logged
  UI->>Sheet: open(isOpen=true)
  Sheet-->>UI: onClose -> set isOpen=false
Loading
sequenceDiagram
  autonumber
  participant Hook as useRealtimeGeolocation
  participant Store as Storage (MMKV)
  participant Updater as LocationServiceUpdater
  participant Hub as GeolocationHub

  Hook->>Store: saveRealtimeGeolocationState(enabled)
  alt updater registered
    Hook->>Updater: locationServiceRealtimeUpdater(enabled)
  end
  alt enabled
    Hook->>Hub: connect()
  else disabled
    Hook->>Hub: disconnect()
  end
Loading
sequenceDiagram
  autonumber
  participant BA as bluetoothAudioService.initialize()
  participant Delay as 500ms timer
  participant Perm as Permissions
  participant Ble as BleManager

  BA->>Delay: wait 500ms
  Delay-->>BA: done
  BA->>Perm: request/check perms
  alt granted
    BA->>Ble: start() & setupEventListeners()
  else denied/failure
    BA-->>BA: skip init (logged)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A hop, a skip, I map the ground,
Realtime whispers all around.
Bells show counts that never stray,
Bluetooth waits a patient delay.
I start, I stop, then nibble away. 🐇📍🔔


📜 Recent 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 371f0fd and 9049f10.

📒 Files selected for processing (2)
  • src/services/__tests__/location.test.ts (21 hunks)
  • src/services/location.ts (8 hunks)
✨ 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

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
🪧 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 or @coderabbit 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

@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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/components/notifications/NotificationButton.tsx (1)

22-33: Add accessibilityRole/label and badge testID; localize "99+".
Improve a11y, testability, and i18n without changing visuals.

-    <Pressable onPress={onPress} className="mr-2 p-2" testID="notification-button">
+    <Pressable
+      onPress={onPress}
+      accessibilityRole="button"
+      accessibilityLabel={t('notifications.buttonLabel', { count: unread, defaultValue: unread > 0 ? `Notifications, ${unread} unread` : 'Notifications' })}
+      className="mr-2 p-2"
+      hitSlop={8}
+      testID="notification-button"
+    >
       <View className="relative">
         <BellIcon size={24} className="text-gray-700 dark:text-gray-300" strokeWidth={2} />
 
-        {counts?.[0]?.count && counts?.[0]?.count > 0 ? (
-          <View className="absolute -right-2 -top-2 flex size-5 items-center justify-center rounded-full bg-red-500">
-            <Text className="text-xs font-bold text-white">{counts?.[0]?.count > 99 ? '99+' : counts?.[0]?.count}</Text>
+        {unread > 0 ? (
+          <View testID="notification-badge" className="absolute -right-2 -top-2 flex size-5 items-center justify-center rounded-full bg-red-500">
+            <Text className="text-xs font-bold text-white">
+              {unread > 99 ? t('notifications.ninetyNinePlus', '99+') : unread}
+            </Text>
           </View>
         ) : null}

Outside-range additions required:

// imports
import { useTranslation } from 'react-i18next';

// inside component, before return
const { t } = useTranslation();
src/services/bluetooth-audio.service.ts (2)

346-350: checkBluetoothState likely misuses BleManager.checkState (returns void in most versions).

Awaiting checkState and mapping its return risks always reading undefined -> State.Unknown. Prefer getState (if available) or listen once for onDidUpdateState after checkState.

Replace with:

async checkBluetoothState(): Promise<State> {
  try {
    const maybeGetState = (BleManager as any).getState;
    if (typeof maybeGetState === 'function') {
      const s = (await maybeGetState()) as BleState;
      return this.mapBleStateToState(s);
    }
    await BleManager.checkState();
    const s: BleState | 'unknown' = await new Promise((resolve) => {
      const sub = BleManager.onDidUpdateState(({ state }: { state: BleState }) => {
        sub.remove();
        resolve(state);
      });
      setTimeout(() => {
        sub.remove();
        resolve('unknown');
      }, 1500);
    });
    return this.mapBleStateToState(s as BleState);
  } catch (error) {
    logger.error({ message: 'Failed to check Bluetooth state', context: { error } });
    return State.Unknown;
  }
}

317-344: Gate BLE permissions by API level and parameterize startup delay

  • Make the startup delay configurable (startupDelayMs: number = 500) instead of hard-coding 500 ms.
  • For Android API ≤ 30 (Android 10/11), request PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION; for API ≥ 31 (Android 12+), request BLUETOOTH_SCAN & BLUETOOTH_CONNECT.
  • (Optional) Pre-check with PermissionsAndroid.check() and return early if already granted.
-  async requestPermissions(): Promise<boolean> {
-    // Add delay to prevent conflicts with other permission requests during app startup
-    await new Promise((resolve) => setTimeout(resolve, 500));
+  async requestPermissions(startupDelayMs: number = 500): Promise<boolean> {
+    // Small startup delay to avoid contention with other permission prompts
+    await new Promise((resolve) => setTimeout(resolve, startupDelayMs));

     if (Platform.OS === 'android') {
       try {
-        const permissions = [PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT];
+        const apiLevel = typeof Platform.Version === 'number' ? Platform.Version : parseInt(String(Platform.Version), 10);
+        const permissions =
+          apiLevel >= 31
+            ? [PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT]
+            : [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];

         const results = await PermissionsAndroid.requestMultiple(permissions);
src/app/login/login-form.tsx (4)

101-111: Fix field-level validation: current schema usage will fail username validation due to missing password.

You're calling loginFormSchema.parseAsync({ username: value }) which requires password too, causing wrong errors. Let zodResolver handle field validation and remove custom rules.

Apply this diff to remove redundant rules for username:

-          <Controller
+          <Controller
             defaultValue=""
             name="username"
             control={control}
-            rules={{
-              validate: async (value) => {
-                try {
-                  await loginFormSchema.parseAsync({ username: value });
-                  return true;
-                } catch (error: any) {
-                  return error.message;
-                }
-              },
-            }}
             render={({ field: { onChange, onBlur, value } }) => (

141-149: Same issue for password Controller. Remove custom rules.

Let the resolver surface errors.

-          <Controller
+          <Controller
             defaultValue=""
             name="password"
             control={control}
-            rules={{
-              validate: async (value) => {
-                try {
-                  await loginFormSchema.parseAsync({ password: value });
-                  return true;
-                } catch (error: any) {
-                  return error.message;
-                }
-              },
-            }}
             render={({ field: { onChange, onBlur, value } }) => (

53-56: Remove unused local validation state and non-localized fallback text.

validated is never updated; it gates errors and adds an unlocalized message. Rely on resolver errors only and drop the fallback string.

-  const [validated] = useState({
-    usernameValid: true,
-    passwordValid: true,
-  });
+  // removed unused local validation flags

-        <FormControl isInvalid={!!errors?.username || !validated.usernameValid} className="w-full">
+        <FormControl isInvalid={!!errors?.username} className="w-full">
...
-            <FormControlErrorText className="text-red-500">{errors?.username?.message || (!validated.usernameValid && 'Username not found')}</FormControlErrorText>
+            <FormControlErrorText className="text-red-500">{errors?.username?.message}</FormControlErrorText>
...
-        <FormControl isInvalid={!!errors.password || !validated.passwordValid} className="w-full">
+        <FormControl isInvalid={!!errors?.password} className="w-full">
...
-            <FormControlErrorText className="text-red-500">{errors?.password?.message || (!validated.passwordValid && t('login.password_incorrect'))}</FormControlErrorText>
+            <FormControlErrorText className="text-red-500">{errors?.password?.message}</FormControlErrorText>

Also applies to: 93-97, 126-129, 132-136, 170-172


87-91: Internationalize all user-facing strings and fix typos.

Hardcoded "Sign In", the description text (typos: “login in to”, “orginization”), and "Log in" bypass i18n. Use existing keys to avoid new translations.

-          <Text className="pb-6 text-center text-4xl font-bold">Sign In</Text>
+          <Text className="pb-6 text-center text-4xl font-bold">{t('login.title')}</Text>
...
-          <Text className="mb-6 max-w-xl text-center text-gray-500">
-            To login in to the Resgrid Responder app, please enter your username and password. Resgrid Responder is an app designed to allow a person to interact with their orginization in the Resgrid system.
-          </Text>
+          <Text className="mb-6 max-w-xl text-center text-gray-500">
+            {t('login.login_button_description')}
+          </Text>
...
-          <Button className="mt-8 w-full" variant="solid" action="primary" onPress={handleSubmit(onSubmit)}>
-            <ButtonText>Log in</ButtonText>
+          <Button className="mt-8 w-full" variant="solid" action="primary" onPress={handleSubmit(onSubmit)}>
+            <ButtonText>{t('login.login_button')}</ButtonText>
           </Button>

Also applies to: 182-183

src/services/__tests__/location.test.ts (1)

467-469: Same: avoid runtime use of erased TS types in matchers.

-      expect(mockSetUnitLocation).toHaveBeenCalledWith(expect.any(SaveUnitLocationInput));
+      expect(mockSetUnitLocation).toHaveBeenCalled();
🧹 Nitpick comments (22)
src/components/notifications/NotificationButton.tsx (2)

12-21: Compute unread once to avoid repeated optional chaining.
Small readability/perf win.

   const { counts, isLoading } = useCounts({
     filters: [
       {
         read: false,
       },
     ],
   });
 
-  if (isLoading) return <ActivityIndicator />;
+  const unread = counts?.[0]?.count ?? 0;
+  if (isLoading) return <ActivityIndicator testID="notifications-loading" accessibilityLabel={t('notifications.loading', 'Loading notifications')} />;

11-11: Type the component as React.FC per repo guidelines.

-export const NotificationButton = ({ onPress }: NotificationButtonProps) => {
+export const NotificationButton: React.FC<NotificationButtonProps> = ({ onPress }) => {
src/components/notifications/__tests__/NotificationButton.test.tsx (4)

28-39: Verify loading indicator presence (more robust).
If you adopt the proposed testID, assert the spinner shows instead of inferring via absence.

-  // Since ActivityIndicator doesn't have a testID, we check that no notification button is rendered
-  expect(screen.queryByTestId('notification-button')).toBeNull();
+  expect(screen.getByTestId('notifications-loading')).toBeTruthy();
+  expect(screen.queryByTestId('notification-button')).toBeNull();

41-56: Avoid coupling tests to exact className strings.
Asserting the icon’s accessibilityLabel equals a specific class list is brittle. Prefer structural checks (icon rendered) and, if needed, assert presence of a semantic a11y label (e.g., “Notifications”) once you add it on the button.


116-129: Duplicate intent with prior color test.
This “Dark Mode Support” test repeats the class assertion; consider consolidating into a single test.


69-90: Add badge test via testID for clarity.
If you add testID="notification-badge", assert badge presence/absence directly instead of querying text.

Example:

render(<NotificationButton onPress={mockOnPress} />);
-expect(screen.getByText('5')).toBeTruthy();
+expect(screen.getByTestId('notification-badge')).toBeTruthy();

And for zero:

render(<NotificationButton onPress={mockOnPress} />);
-expect(screen.queryByText('0')).toBeNull();
+expect(screen.queryByTestId('notification-badge')).toBeNull();

Also applies to: 81-90

src/services/bluetooth-audio.service.ts (1)

391-403: Avoid double “scan stopped” handling; clear scan timeout when stop event fires.

You both rely on onStopScan and also fire a local timeout; today the timeout remains pending and triggers a second stop/log.

Apply this diff:

   private handleScanStopped(): void {
+    if (this.scanTimeout) {
+      clearTimeout(this.scanTimeout);
+      this.scanTimeout = null;
+    }
     useBluetoothAudioStore.getState().setIsScanning(false);
     logger.info({
       message: 'Bluetooth scan stopped',
     });
   }

Also applies to: 276-281

src/services/__tests__/bluetooth-audio.service.test.ts (3)

34-58: Augment Platform mock with Version and avoid the bare side-effect import.

Add Platform.Version to support API-gated permission tests, and drop the top-level import 'react-native' to prevent loading the real module before mocks.

Apply this diff to the mock:

   Platform: {
     OS: 'android',
+    Version: 33,
   },

Then remove Line 2 (import 'react-native';) or move all mocks above it.


91-115: Strengthen assertion: verify requested permission set and remove unused fine location in the grant map.

Assert the exact array passed to requestMultiple and align the mock results for clarity.

   mockRequestMultiple.mockResolvedValue({
-    'android.permission.BLUETOOTH_SCAN': 'granted',
-    'android.permission.BLUETOOTH_CONNECT': 'granted',
-    'android.permission.ACCESS_FINE_LOCATION': 'granted',
+    'android.permission.BLUETOOTH_SCAN': 'granted',
+    'android.permission.BLUETOOTH_CONNECT': 'granted',
   });
   ...
   const result = await permissionPromise;
   expect(result).toBe(true);
-  expect(mockRequestMultiple).toHaveBeenCalled();
+  expect(mockRequestMultiple).toHaveBeenCalledWith([
+    PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
+    PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
+  ]);

154-174: Denied permissions: add argument assertion and add a pre-Android 12 case.

Also verify the requested array, and (after implementing API gating) cover API 30 with ACCESS_FINE_LOCATION.

Minimal additions:

expect(result).toBe(false);
+expect(PermissionsAndroid.requestMultiple).toHaveBeenCalledWith([
+  PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
+  PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
+]);

I can add a new test:

it('requests ACCESS_FINE_LOCATION on API 30', async () => {
  (Platform as any).OS = 'android';
  (Platform as any).Version = 30;
  const mockRequestMultiple = PermissionsAndroid.requestMultiple as jest.Mock;
  mockRequestMultiple.mockResolvedValue({
    'android.permission.ACCESS_FINE_LOCATION': 'granted',
  });
  const service = bluetoothAudioService as any;
  const p = service.requestPermissions();
  jest.advanceTimersByTime(500);
  await p;
  expect(mockRequestMultiple).toHaveBeenCalledWith([
    PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
  ]);
});

Want me to push the test along with the service change?

src/app/login/login-form.tsx (3)

71-81: Make analytics call promise-safe; current try/catch won’t catch async rejections.

Wrap with Promise.resolve(...).catch(...) (or await inside an async callback).

-  const handleServerUrlPress = useCallback(() => {
-    try {
-      trackEvent('login_server_url_pressed', {
-        timestamp: new Date().toISOString(),
-      });
-    } catch (error) {
-      console.warn('Failed to track login server URL press analytics:', error);
-    }
-    setShowServerUrl(true);
-  }, [trackEvent]);
+  const handleServerUrlPress = useCallback(() => {
+    Promise.resolve(
+      trackEvent('login_server_url_pressed', { timestamp: new Date().toISOString() })
+    ).catch((error) => {
+      console.warn('Failed to track login server URL press analytics:', error);
+    });
+    setShowServerUrl(true);
+  }, [trackEvent]);

186-189: Add accessibility and testing hooks to the new button.

Improves a11y and simplifies test selection.

-        <Button className="mt-14 w-full" variant="outline" onPress={handleServerUrlPress}>
+        <Button
+          className="mt-14 w-full"
+          variant="outline"
+          onPress={handleServerUrlPress}
+          accessibilityRole="button"
+          accessibilityLabel={t('login.change_server_url')}
+          testID="changeServerUrlButton"
+        >
           <ButtonText className="text-sm">{t('login.change_server_url')}</ButtonText>
         </Button>

42-42: Type the component as React.FC for consistency with guidelines.

Aligns with repo guidance.

-export const LoginForm = ({ onSubmit = () => { }, isLoading = false, error = undefined }: LoginFormProps) => {
+export const LoginForm: React.FC<LoginFormProps> = ({ onSubmit = () => {}, isLoading = false, error }) => {
src/app/(app)/map.tsx (1)

107-110: Remove no-op useEffect; centralize documentation elsewhere.
This effect does nothing. Drop it to reduce render overhead and align with “minimize useEffect” guideline. Also confirm that app-level tracking now surfaces permission/error feedback to the user (toast/snackbar) since the per-screen toast was removed.

-  useEffect(() => {
-    // Location tracking is now handled at the app level when user is logged in
-    // No need to start/stop here as it should persist across screen changes
-  }, []);
src/app/(app)/_layout.tsx (1)

111-124: startLocationUpdates invoked only once; add user-facing error notification

  • Verified no other call sites in the codebase.
  • On failure, show a user-visible notification (e.g. toast/snackbar) alongside the existing log.
src/services/__tests__/location.test.ts (1)

106-106: Remove now-unused import after matcher change.
Prevents linter errors.

-import { SaveUnitLocationInput } from '@/models/v4/unitLocation/saveUnitLocationInput';
src/lib/hooks/use-realtime-geolocation.ts (2)

8-9: Avoid double writes to MMKV storage.
useMMKVBoolean’s setter already persists; calling saveRealtimeGeolocationState writes again. Consider removing the extra write for clarity and fewer I/Os. If you keep it for consistency, note it’s redundant.

-import { getRealtimeGeolocationStorageKey, saveRealtimeGeolocationState } from '../storage/realtime-geolocation';
+import { getRealtimeGeolocationStorageKey } from '../storage/realtime-geolocation';

35-45: Remove redundant save; rely on the hook setter.

       try {
         _setRealtimeGeolocationEnabled(enabled);
-        saveRealtimeGeolocationState(enabled);
 
         // Update the location service if the updater is registered
         if (locationServiceRealtimeUpdater) {
           await locationServiceRealtimeUpdater(enabled);
         }
src/lib/hooks/__tests__/use-realtime-geolocation.test.ts (1)

45-58: Reset registered updater between tests to avoid cross-test coupling.
Keeps module-level state clean regardless of test order.

   beforeEach(() => {
     jest.clearAllMocks();
@@
     mockUseSignalRStore.mockReturnValue({
       isGeolocationHubConnected: false,
       connectGeolocationHub: mockConnectGeolocationHub,
       disconnectGeolocationHub: mockDisconnectGeolocationHub,
     });
   });
+
+  afterEach(() => {
+    // Ensure no updater leak across tests
+    registerLocationServiceRealtimeUpdater(null as any);
+  });
src/services/location.ts (3)

107-113: Background task: avoid storage read on every tick (optional).

loadRealtimeGeolocationState() per update adds I/O overhead. Cache the value with a short TTL or memoize in a module-level var and refresh periodically; background toggles will still propagate because the UI persists to storage.


188-209: DRY up background task options and ensure consistent permission checks.

  • You check background permission here (good), but updateBackgroundGeolocationSetting() registers the task without rechecking permission—mirror this guard there.
  • Extract the task options into a shared constant to avoid drift across call sites.

Example (outside this file’s changed lines):

// top-level
const BACKGROUND_TASK_OPTIONS: Location.LocationTaskOptions = {
  accuracy: Location.Accuracy.Balanced,
  timeInterval: 15000,
  distanceInterval: 10,
  foregroundService: {
    notificationTitle: 'Location Tracking',
    notificationBody: 'Tracking your location in the background',
  },
};

Then use BACKGROUND_TASK_OPTIONS in both registration sites.

If you want, I can generate a small patch updating both sites and adding the missing permission guard in updateBackgroundGeolocationSetting().


227-227: Consider deduping foreground sends.

Add a small “significant movement” or “min interval” gate to avoid spamming the API when jitter causes frequent foreground points with minimal movement.

📜 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 dda84f5 and 02ae52e.

📒 Files selected for processing (15)
  • src/app/(app)/_layout.tsx (3 hunks)
  • src/app/(app)/map.tsx (1 hunks)
  • src/app/login/__tests__/login-form.test.tsx (1 hunks)
  • src/app/login/login-form.tsx (5 hunks)
  • src/components/notifications/NotificationButton.tsx (1 hunks)
  • src/components/notifications/__tests__/NotificationButton.test.tsx (1 hunks)
  • src/lib/hooks/__tests__/use-realtime-geolocation.test.ts (1 hunks)
  • src/lib/hooks/use-realtime-geolocation.ts (1 hunks)
  • src/services/__tests__/bluetooth-audio.service.test.ts (2 hunks)
  • src/services/__tests__/location.test.ts (19 hunks)
  • src/services/bluetooth-audio.service.ts (2 hunks)
  • src/services/location.ts (11 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 (7)
src/**

📄 CodeRabbit inference engine (.cursorrules)

src/**: Organize files by feature, grouping related components, hooks, and styles
Directory and file names should be lowercase and hyphenated (e.g., user-profile)

Files:

  • src/translations/en.json
  • src/translations/ar.json
  • src/app/(app)/map.tsx
  • src/lib/hooks/__tests__/use-realtime-geolocation.test.ts
  • src/app/login/__tests__/login-form.test.tsx
  • src/components/notifications/__tests__/NotificationButton.test.tsx
  • src/services/__tests__/location.test.ts
  • src/services/bluetooth-audio.service.ts
  • src/app/login/login-form.tsx
  • src/components/notifications/NotificationButton.tsx
  • src/translations/es.json
  • src/services/location.ts
  • src/services/__tests__/bluetooth-audio.service.test.ts
  • src/lib/hooks/use-realtime-geolocation.ts
  • src/app/(app)/_layout.tsx
src/translations/**/*.json

📄 CodeRabbit inference engine (.cursorrules)

Store translation dictionary files under src/translations as JSON resources

Files:

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

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Store translation dictionary files in src/translations

Files:

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

📄 CodeRabbit inference engine (.cursorrules)

**/*.{ts,tsx}: Write concise, type-safe TypeScript code
Use camelCase for variable and function names
Use TypeScript for all components and favor interfaces for props and state
Avoid using any; use precise types
Use React Navigation for navigation and deep linking following best practices
Handle errors gracefully and provide user feedback
Implement proper offline support (caching, queueing, retries)
Use Expo SecureStore for sensitive data storage
Use zustand for state management
Use react-hook-form for form handling
Use react-query for data fetching and caching
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
Ensure support for dark mode and light mode
Handle errors gracefully and provide user feedback
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/app/(app)/map.tsx
  • src/lib/hooks/__tests__/use-realtime-geolocation.test.ts
  • src/app/login/__tests__/login-form.test.tsx
  • src/components/notifications/__tests__/NotificationButton.test.tsx
  • src/services/__tests__/location.test.ts
  • src/services/bluetooth-audio.service.ts
  • src/app/login/login-form.tsx
  • src/components/notifications/NotificationButton.tsx
  • src/services/location.ts
  • src/services/__tests__/bluetooth-audio.service.test.ts
  • src/lib/hooks/use-realtime-geolocation.ts
  • src/app/(app)/_layout.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursorrules)

**/*.tsx: Use functional components and React hooks instead of class components
Use PascalCase for React component names
Use React.FC for defining functional components with props
Minimize useEffect/useState usage and avoid heavy computations during render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Provide getItemLayout to FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers; define callbacks with useCallback or outside render
Use gluestack-ui for styling where available from components/ui; otherwise, style via StyleSheet.create or styled-components
Ensure responsive design across screen sizes and orientations
Use react-native-fast-image for image handling instead of the default Image where appropriate
Wrap all user-facing text in t() from react-i18next for translations
Support dark mode and light mode in UI components
Use @rnmapbox/maps for maps or navigation features
Use lucide-react-native for icons directly; do not use the gluestack-ui icon component
Use conditional rendering with the ternary operator (?:) instead of &&

**/*.tsx: Use functional components and hooks over class components
Ensure components are modular, reusable, and maintainable
Ensure all components are mobile-friendly, responsive, and support both iOS and Android
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 to prevent re-renders
Ensure responsive design for different screen sizes and orientations
Optimize image handling using rea...

Files:

  • src/app/(app)/map.tsx
  • src/app/login/__tests__/login-form.test.tsx
  • src/components/notifications/__tests__/NotificationButton.test.tsx
  • src/app/login/login-form.tsx
  • src/components/notifications/NotificationButton.tsx
  • src/app/(app)/_layout.tsx
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{test,spec}.{ts,tsx}: Create Jest tests for all generated components, services, and logic
Ensure tests run without errors and fix failing tests

Files:

  • src/lib/hooks/__tests__/use-realtime-geolocation.test.ts
  • src/app/login/__tests__/login-form.test.tsx
  • src/components/notifications/__tests__/NotificationButton.test.tsx
  • src/services/__tests__/location.test.ts
  • src/services/__tests__/bluetooth-audio.service.test.ts
src/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.test.{ts,tsx}: Create and use Jest tests to validate all generated components
Generate tests for all components, services, and logic; ensure tests run without errors

Files:

  • src/lib/hooks/__tests__/use-realtime-geolocation.test.ts
  • src/app/login/__tests__/login-form.test.tsx
  • src/components/notifications/__tests__/NotificationButton.test.tsx
  • src/services/__tests__/location.test.ts
  • src/services/__tests__/bluetooth-audio.service.test.ts
🧠 Learnings (3)
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to **/*.tsx : Use rnmapbox/maps for maps, mapping, or vehicle navigation

Applied to files:

  • src/app/(app)/map.tsx
📚 Learning: 2025-08-21T02:46:42.743Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .cursorrules:0-0
Timestamp: 2025-08-21T02:46:42.743Z
Learning: Applies to **/*.tsx : Use rnmapbox/maps for maps or navigation features

Applied to files:

  • src/app/(app)/map.tsx
📚 Learning: 2025-08-21T02:47:24.081Z
Learnt from: CR
PR: Resgrid/Responder#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-21T02:47:24.081Z
Learning: Applies to src/**/*.test.{ts,tsx} : Create and use Jest tests to validate all generated components

Applied to files:

  • src/app/login/__tests__/login-form.test.tsx
🧬 Code graph analysis (8)
src/lib/hooks/__tests__/use-realtime-geolocation.test.ts (2)
src/lib/storage/realtime-geolocation.ts (1)
  • saveRealtimeGeolocationState (30-32)
src/lib/hooks/use-realtime-geolocation.ts (2)
  • useRealtimeGeolocation (30-75)
  • registerLocationServiceRealtimeUpdater (20-22)
src/app/login/__tests__/login-form.test.tsx (1)
src/app/login/login-form.tsx (1)
  • LoginForm (42-195)
src/components/notifications/__tests__/NotificationButton.test.tsx (1)
src/components/notifications/NotificationButton.tsx (1)
  • NotificationButton (11-35)
src/services/__tests__/location.test.ts (4)
src/lib/hooks/use-realtime-geolocation.ts (1)
  • registerLocationServiceRealtimeUpdater (20-22)
src/lib/storage/background-geolocation.ts (1)
  • loadBackgroundGeolocationState (16-31)
src/lib/storage/realtime-geolocation.ts (1)
  • loadRealtimeGeolocationState (10-25)
src/services/location.ts (1)
  • locationService (366-366)
src/services/location.ts (2)
src/lib/storage/realtime-geolocation.ts (1)
  • loadRealtimeGeolocationState (10-25)
src/lib/hooks/use-realtime-geolocation.ts (1)
  • registerLocationServiceRealtimeUpdater (20-22)
src/services/__tests__/bluetooth-audio.service.test.ts (1)
src/services/bluetooth-audio.service.ts (1)
  • bluetoothAudioService (1686-1686)
src/lib/hooks/use-realtime-geolocation.ts (2)
src/lib/storage/realtime-geolocation.ts (2)
  • getRealtimeGeolocationStorageKey (37-39)
  • saveRealtimeGeolocationState (30-32)
src/lib/storage/index.tsx (1)
  • storage (5-5)
src/app/(app)/_layout.tsx (1)
src/services/location.ts (1)
  • locationService (366-366)
⏰ 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 (15)
src/components/notifications/NotificationButton.tsx (1)

25-25: Icon color swap aligns with dark/light contrast — LGTM.
The new gray palette improves contrast and adheres to dark-mode guidelines.

src/services/bluetooth-audio.service.ts (1)

85-90: Good: initialize only after permissions.

Deferring BleManager.start and listener setup until permissions are granted is correct and avoids spurious native errors.

src/services/__tests__/bluetooth-audio.service.test.ts (3)

65-70: Timers: OK.

Using fake timers to drive the startup delay is correct; restoring after each test prevents leakage.


116-136: iOS path: OK.

Bypassing PermissionsAndroid and returning true (after the delay) is fine; you also reset Platform.OS for subsequent tests.


137-153: Failure path: OK.

Reject → false is correct and observable with the current logger.

src/translations/en.json (2)

526-526: LGTM: new key added and used by UI.

Key login.change_server_url looks correct and consistent with settings terminology.


526-526: All locale files include login.change_server_url. Verification script exited cleanly with no missing keys—no further action needed.

src/translations/es.json (1)

526-526: LGTM: Spanish translation is clear and idiomatic.

Cambiar URL del servidor matches the English intent.

src/translations/ar.json (1)

526-526: LGTM: Arabic translation added.

Key aligns with English. Consider future RTL layout checks when this label appears in buttons.

src/services/__tests__/location.test.ts (1)

374-377: Keep the existing matcher; SaveUnitLocationInput is a runtime class
The SaveUnitLocationInput symbol is exported as a class (not erased), so expect.any(SaveUnitLocationInput) will work as intended.

src/lib/hooks/__tests__/use-realtime-geolocation.test.ts (1)

1-171: Solid coverage and behavior assertions.
Mocks and flows exercise enable/disable, updater registration, error paths, and logger outputs well.

src/services/location.ts (4)

9-13: Realtime updater wiring looks good.

Registering updateRealtimeGeolocationSetting via the hook keeps the service reactive to runtime changes without circular deps.


123-130: Realtime state field and updater registration: LGTM.

Keeps foreground watcher aware of runtime setting changes.


181-184: Loading both background and realtime settings on start: LGTM.

Keeps runtime flags in sync on boot.


233-236: Startup log context: LGTM.

Helpful visibility for toggles after boot.

Copy link

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/app/login/login-form.tsx (1)

21-32: Localize validation: use translation keys and render via t().

Avoid hardcoded English in schema; surface keys and translate at render.

 const loginFormSchema = z.object({
   username: z
     .string({
-      required_error: 'Username is required',
+      required_error: 'form.required',
     })
-    .min(3, 'Username must be at least 3 characters'),
+    .min(3, { message: 'login.username_min_length' }),
   password: z
     .string({
-      required_error: 'Password is required',
+      required_error: 'form.required',
     })
-    .min(6, 'Password must be at least 6 characters'),
+    .min(6, { message: 'login.password_min_length' }),
 });
-            <FormControlErrorText className="text-red-500">{errors?.username?.message}</FormControlErrorText>
+            <FormControlErrorText className="text-red-500">
+              {errors?.username?.message ? t(errors.username.message as string) : null}
+            </FormControlErrorText>
-            <FormControlErrorText className="text-red-500">{errors?.password?.message}</FormControlErrorText>
+            <FormControlErrorText className="text-red-500">
+              {errors?.password?.message ? t(errors.password.message as string) : null}
+            </FormControlErrorText>

Also applies to: 112-113, 145-146

src/translations/es.json (1)

520-539: Add validation strings for new schema keys.

Required by the localized zod messages.

   "login": {
     "change_server_url": "Cambiar URL del servidor",
@@
     "password": "Contraseña",
     "password_incorrect": "La contraseña era incorrecta",
     "password_placeholder": "Introduce tu contraseña",
+    "password_min_length": "La contraseña debe tener al menos 6 caracteres",
     "title": "Iniciar sesión",
     "username": "Nombre de usuario",
-    "username_placeholder": "Introduce tu nombre de usuario"
+    "username_placeholder": "Introduce tu nombre de usuario",
+    "username_min_length": "El nombre de usuario debe tener al menos 3 caracteres"
   },
src/services/location.ts (1)

279-306: Reflect background-enabled state in the store when enabling.
You flip isBackgroundGeolocationEnabled and register the task but never set locationStore.setBackgroundEnabled(true). Set it after permission grant so UI/state stay consistent.

     if (enabled) {
       // Request background permissions when enabling background geolocation
       const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync();
       if (backgroundStatus !== 'granted') {
         logger.warn({
           message: 'Background location permission not granted, cannot enable background geolocation',
           context: { backgroundStatus },
         });
         this.isBackgroundGeolocationEnabled = false;
         return;
       }
+      // Reflect background tracking enabled in app state
+      useLocationStore.getState().setBackgroundEnabled(true);
♻️ Duplicate comments (2)
src/app/login/__tests__/login-form.test.tsx (1)

1-12: Good: Top-level mocks and user interaction assertions address prior feedback.

Mocks are now effective; the button press triggers analytics and opens the sheet. This directly verifies the workflow.

Also applies to: 29-45, 61-67, 69-88

src/services/location.ts (1)

45-50: PII-safe logging on realtime-disabled path — good.
You removed coordinates from the disabled branch as previously requested.

🧹 Nitpick comments (12)
src/app/login/__tests__/login-form.test.tsx (3)

55-59: Remove redundant mockClear calls.

jest.clearAllMocks() already clears all jest.fn calls. The extra mockClear calls are unnecessary.

   beforeEach(() => {
     jest.clearAllMocks();
-    mockTrackEvent.mockClear();
-    mockServerUrlBottomSheet.mockClear();
   });

69-88: Add a test to assert onClose closes the sheet.

This verifies the callback wiring.

   it('should track analytics and show bottom sheet when server URL button is pressed', () => {
     render(<LoginForm onSubmit={mockOnSubmit} />);
@@
     );
   });
+
+  it('closes the ServerUrlBottomSheet when onClose is invoked', () => {
+    render(<LoginForm onSubmit={mockOnSubmit} />);
+    fireEvent.press(screen.getByText('login.change_server_url'));
+    const openProps = mockServerUrlBottomSheet.mock.calls.at(-1)?.[0];
+    expect(openProps?.isOpen).toBe(true);
+    // trigger close
+    openProps?.onClose();
+    const closedProps = mockServerUrlBottomSheet.mock.calls.at(-1)?.[0];
+    expect(closedProps?.isOpen).toBe(false);
+  });

90-116: Add submit test with valid credentials; prune unused error-prop rerender.

Covers the happy path; avoids relying on an unused prop.

   it('should render with loading state', () => {
     render(<LoginForm onSubmit={mockOnSubmit} isLoading={true} />);
     expect(screen.getByText('login.login_button_loading')).toBeTruthy();
   });
 
-  it('should render with different prop combinations', () => {
-    const { rerender } = render(
-      <LoginForm
-        onSubmit={mockOnSubmit}
-        isLoading={false}
-        error={undefined}
-      />
-    );
-    expect(screen.getByText('login.title')).toBeTruthy();
-
-    // Test with error
-    rerender(
-      <LoginForm
-        onSubmit={mockOnSubmit}
-        error="Test error"
-      />
-    );
-    expect(screen.getByText('login.title')).toBeTruthy();
-  });
+  it('submits when fields are valid', () => {
+    render(<LoginForm onSubmit={mockOnSubmit} />);
+    fireEvent.changeText(screen.getByPlaceholderText('login.username_placeholder'), 'user123');
+    fireEvent.changeText(screen.getByPlaceholderText('login.password_placeholder'), 'secret123');
+    fireEvent.press(screen.getByText('login.login_button'));
+    expect(mockOnSubmit).toHaveBeenCalledWith(
+      expect.objectContaining({ username: 'user123', password: 'secret123' }),
+      expect.anything()
+    );
+  });
src/app/login/login-form.tsx (4)

36-42: Drop unused error prop; type the component as React.FC.

error isn’t rendered anymore; remove it to avoid confusion and align with guidelines (use React.FC).

 export type LoginFormProps = {
   onSubmit?: SubmitHandler<FormType>;
   isLoading?: boolean;
-  error?: string;
 };
 
-export const LoginForm = ({ onSubmit = () => { }, isLoading = false, error = undefined }: LoginFormProps) => {
+export const LoginForm: React.FC<LoginFormProps> = ({ onSubmit = () => {}, isLoading = false }) => {

57-61: Stabilize handlers; avoid creating functions during render.

Memoize toggles and submit handler to reduce re-renders and adhere to handler guidelines.

-  const handleState = () => {
-    setShowPassword((showState) => {
-      return !showState;
-    });
-  };
+  const togglePasswordVisibility = useCallback(() => {
+    setShowPassword((prev) => !prev);
+  }, []);
-                <InputSlot onPress={handleState} className="pr-3">
+                <InputSlot onPress={togglePasswordVisibility} className="pr-3">
+  const onLoginPress = useCallback(() => handleSubmit(onSubmit)(), [handleSubmit, onSubmit]);
-          <Button className="mt-8 w-full" variant="solid" action="primary" onPress={handleSubmit(onSubmit)}>
+          <Button className="mt-8 w-full" variant="solid" action="primary" onPress={onLoginPress}>
             <ButtonText>{t('login.login_button')}</ButtonText>
           </Button>

Also applies to: 137-139, 149-158, 66-66, 77-77


101-106: Use platform autoComplete hints.

Improve UX by using specific autocomplete modes.

-                  autoComplete="off"
+                  autoComplete="username"
-                  autoComplete="off"
+                  autoComplete="password"

Also applies to: 133-136


42-42: Fix linter warning about stray whitespace.

Line 42: remove the extra space flagged by the test check.

src/services/__tests__/location.test.ts (2)

152-164: Construction-time updater registrations verified.
Lightweight boolean gate works here; consider asserting exact call counts in the dedicated test (see comment below) to avoid reliance on beforeAll state.

- expect(registrationCallsVerified).toBe(true);
+ expect(mockRegisterLocationServiceUpdater).toHaveBeenCalledTimes(1);
+ expect(mockRegisterLocationServiceRealtimeUpdater).toHaveBeenCalledTimes(1);

258-274: Add regression guard: ensure no background prompt during foreground-only permission flow.
Requesting background permission here is irrelevant; assert it wasn’t called to prevent future regressions.

   const result = await locationService.requestPermissions();
   expect(result).toBe(true);
+  expect(mockLocation.requestBackgroundPermissionsAsync).not.toHaveBeenCalled();
src/services/location.ts (3)

265-274: Avoid awaiting a sync function (saveRealtimeGeolocationState).
saveRealtimeGeolocationState returns void; awaiting it is misleading and may trigger lint warnings. Either remove await or make the saver async.

-    await saveRealtimeGeolocationState(enabled);
+    saveRealtimeGeolocationState(enabled);

Alternatively, change saveRealtimeGeolocationState to return Promise and keep await.


63-67: Tighten numeric validation to avoid coercion edge cases.
Using global isFinite can coerce strings; prefer typeof + Number.isFinite to keep types strict.

// Suggested helper update (outside this hunk):
const safeNumericString = (value: number | null | undefined, field: string): string => {
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    logger.warn({ message: `Invalid ${field} value: ${value}, defaulting to '0'` });
    return '0';
  }
  return String(value);
};

112-118: Process all reported locations from TaskManager, not just the first.
Expo can batch multiple points; currently only locations[0] is handled. Iterate to avoid drops.

// Suggested change (broader than this hunk):
const { locations } = data as { locations: Location.LocationObject[] };
for (const loc of locations) {
  logger.info({
    message: 'Background location update received',
    context: { latitude: loc.coords.latitude, longitude: loc.coords.longitude, heading: loc.coords.heading },
  });
  useLocationStore.getState().setLocation(loc);
  const isRealtimeEnabled = await loadRealtimeGeolocationState();
  await sendLocationToAPI(loc, isRealtimeEnabled);
}
📜 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 02ae52e and 371f0fd.

📒 Files selected for processing (10)
  • src/app/(app)/_layout.tsx (3 hunks)
  • src/app/login/__tests__/login-form.test.tsx (1 hunks)
  • src/app/login/login-form.tsx (6 hunks)
  • src/components/notifications/NotificationButton.tsx (3 hunks)
  • src/components/notifications/__tests__/NotificationButton.test.tsx (1 hunks)
  • src/services/__tests__/location.test.ts (20 hunks)
  • src/services/location.ts (8 hunks)
  • src/translations/ar.json (2 hunks)
  • src/translations/en.json (2 hunks)
  • src/translations/es.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/translations/ar.json
  • src/components/notifications/tests/NotificationButton.test.tsx
  • src/translations/en.json
  • src/app/(app)/_layout.tsx
  • src/components/notifications/NotificationButton.tsx
🧰 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
Use TypeScript for all components and favor interfaces for props and state
Avoid using any; use precise types
Use React Navigation for navigation and deep linking following best practices
Handle errors gracefully and provide user feedback
Implement proper offline support (caching, queueing, retries)
Use Expo SecureStore for sensitive data storage
Use zustand for state management
Use react-hook-form for form handling
Use react-query for data fetching and caching
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
Ensure support for dark mode and light mode
Handle errors gracefully and provide user feedback
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/app/login/__tests__/login-form.test.tsx
  • src/services/location.ts
  • src/app/login/login-form.tsx
  • src/services/__tests__/location.test.ts
**/*.tsx

📄 CodeRabbit inference engine (.cursorrules)

**/*.tsx: Use functional components and React hooks instead of class components
Use PascalCase for React component names
Use React.FC for defining functional components with props
Minimize useEffect/useState usage and avoid heavy computations during render
Use React.memo for components with static props to prevent unnecessary re-renders
Optimize FlatList with removeClippedSubviews, maxToRenderPerBatch, and windowSize
Provide getItemLayout to FlatList when items have consistent size
Avoid anonymous functions in renderItem or event handlers; define callbacks with useCallback or outside render
Use gluestack-ui for styling where available from components/ui; otherwise, style via StyleSheet.create or styled-components
Ensure responsive design across screen sizes and orientations
Use react-native-fast-image for image handling instead of the default Image where appropriate
Wrap all user-facing text in t() from react-i18next for translations
Support dark mode and light mode in UI components
Use @rnmapbox/maps for maps or navigation features
Use lucide-react-native for icons directly; do not use the gluestack-ui icon component
Use conditional rendering with the ternary operator (?:) instead of &&

**/*.tsx: Use functional components and hooks over class components
Ensure components are modular, reusable, and maintainable
Ensure all components are mobile-friendly, responsive, and support both iOS and Android
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 to prevent re-renders
Ensure responsive design for different screen sizes and orientations
Optimize image handling using rea...

Files:

  • src/app/login/__tests__/login-form.test.tsx
  • src/app/login/login-form.tsx
src/**

📄 CodeRabbit inference engine (.cursorrules)

src/**: Organize files by feature, grouping related components, hooks, and styles
Directory and file names should be lowercase and hyphenated (e.g., user-profile)

Files:

  • src/app/login/__tests__/login-form.test.tsx
  • src/services/location.ts
  • src/app/login/login-form.tsx
  • src/translations/es.json
  • src/services/__tests__/location.test.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

**/*.{test,spec}.{ts,tsx}: Create Jest tests for all generated components, services, and logic
Ensure tests run without errors and fix failing tests

Files:

  • src/app/login/__tests__/login-form.test.tsx
  • src/services/__tests__/location.test.ts
src/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.test.{ts,tsx}: Create and use Jest tests to validate all generated components
Generate tests for all components, services, and logic; ensure tests run without errors

Files:

  • src/app/login/__tests__/login-form.test.tsx
  • src/services/__tests__/location.test.ts
src/translations/**/*.json

📄 CodeRabbit inference engine (.cursorrules)

Store translation dictionary files under src/translations as JSON resources

Files:

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

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Store translation dictionary files in src/translations

Files:

  • src/translations/es.json
🧬 Code graph analysis (2)
src/services/location.ts (3)
src/lib/storage/realtime-geolocation.ts (2)
  • loadRealtimeGeolocationState (10-25)
  • saveRealtimeGeolocationState (30-32)
src/lib/hooks/use-realtime-geolocation.ts (1)
  • registerLocationServiceRealtimeUpdater (20-22)
src/lib/storage/background-geolocation.ts (1)
  • loadBackgroundGeolocationState (16-31)
src/services/__tests__/location.test.ts (4)
src/lib/hooks/use-realtime-geolocation.ts (1)
  • registerLocationServiceRealtimeUpdater (20-22)
src/lib/storage/background-geolocation.ts (1)
  • loadBackgroundGeolocationState (16-31)
src/lib/storage/realtime-geolocation.ts (2)
  • loadRealtimeGeolocationState (10-25)
  • saveRealtimeGeolocationState (30-32)
src/services/location.ts (2)
  • locationService (352-352)
  • sendLocationToAPI (355-355)
🪛 GitHub Check: test
src/app/login/login-form.tsx

[warning] 42-42:
Delete ·

⏰ 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). (2)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
src/translations/es.json (1)

521-539: LGTM: Added login.change_server_url and notifications keys.

Translations read well and align with usage in the LoginForm and settings.

Also applies to: 724-729

src/services/__tests__/location.test.ts (5)

19-21: Mocks align with new realtime + background flows — good coverage.
Realtime updater/storage and getBackgroundPermissionsAsync are mocked correctly; logger.debug added for new paths.

Also applies to: 27-27, 33-36, 64-65, 71-71


347-365: Background task registration gating — LGTM.
Correctly skips registration when background permission is denied and logs a warning.


367-381: Foreground path no longer sends to API — LGTM.
Store update only, with explicit “TaskManager handles API sends” expectation.


441-446: Stop background updates path — LGTM.
State flip only (task handled elsewhere) matches the refactor.


677-699: Realtime setting persistence tests — LGTM.
Verifies in-memory flip and persisted state write.

Comment on lines 41 to 50
// Always update local store regardless of realtime setting
useLocationStore.getState().setLocation(location);

// Only send to API if realtime geolocation is enabled
if (!isRealtimeEnabled) {
logger.debug({
message: 'Realtime geolocation disabled, skipping API call',
});
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove duplicate local store updates.
sendLocationToAPI updates the store, and the TaskManager handler also updates it before calling sendLocationToAPI. This double-writes the same point. Make sendLocationToAPI send-only.

-  // Always update local store regardless of realtime setting
-  useLocationStore.getState().setLocation(location);
+  // Caller is responsible for store updates to avoid duplicates

Also applies to: 112-118

🤖 Prompt for AI Agents
In src/services/location.ts around lines 41-50 (also apply same change at
112-118): sendLocationToAPI currently writes the location into the local store,
causing a duplicate update because callers (TaskManager handler) already set the
store before calling; remove the local store write from sendLocationToAPI so it
becomes send-only (delete the useLocationStore.getState().setLocation(location)
calls in both places), keep the realtime-enabled check and API request logic
intact, and ensure callers remain responsible for updating the local store
before invoking sendLocationToAPI.

@ucswift
Copy link
Member Author

ucswift commented Aug 29, 2025

Approve

Copy link

@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 fbf3e9e into master Aug 29, 2025
10 of 12 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Oct 1, 2025
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