Skip to content

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Aug 17, 2025

Summary by CodeRabbit

  • New Features

    • Contacts pull-to-refresh now bypasses cache to fetch fresh data.
    • Contact notes render smartly: plain text shown natively, HTML shown with improved styling; notes sorted newest first.
    • Added system-config retrieval endpoint for fetching available system locations.
  • UI/UX

    • Server URL bottom sheet is scrollable with improved keyboard and platform handling.
  • Documentation

    • Guide added for Contacts pull-to-refresh and cache behavior.
  • Tests

    • New and updated tests covering pull-to-refresh, store behavior, integration, and server URL sheet.
  • Chores

    • Pinned react-native-callkeep dependency to a specific commit.

@ucswift
Copy link
Member Author

ucswift commented Aug 17, 2025

@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Walkthrough

Adds pull-to-refresh that bypasses contacts cache, introduces getSystemConfig API and supporting models, adjusts imports, enhances contact notes rendering and Settings bottom-sheet, pins react-native-callkeep to a specific commit, and adds/updates tests and documentation.

Changes

Cohort / File(s) Summary of changes
Contacts pull-to-refresh (cache bypass)
docs/contacts-pull-to-refresh-implementation.md, src/api/contacts/contacts.ts, src/stores/contacts/store.ts, src/app/(app)/contacts.tsx, src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx, src/app/(app)/__tests__/contacts.test.tsx, src/stores/contacts/__tests__/store.test.ts
Add optional forceRefresh flag to contacts API/store; component calls fetchContacts(true) on user refresh; cache invalidation via cacheManager.remove(...); tests and docs added/updated to verify default vs forced behavior.
System config API + barrel import alignment
src/api/config/index.ts, src/models/v4/configs/..., src/stores/app/core-store.ts, src/stores/app/__tests__/core-store.test.ts
Add getSystemConfig() API with cached endpoint (ttl=1 day, enabled=false) and new models GetSystemConfigResult / GetSystemConfigResultData / ResgridSystemLocation; switch some imports/mocks to @/api/config.
Settings bottom sheet updates
src/components/settings/server-url-bottom-sheet.tsx, src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
Wrap content in ScrollView, add keyboard handling/options, snapPoints, platform-aware insets, improved input props; tests added to verify open/closed rendering and input keyboard props.
Contact notes rendering
src/components/contacts/contact-notes-list.tsx
Detect plain text vs HTML notes; render Text for plain text or WebView for HTML; add safe WebView settings, external link handling via Linking, themed injected HTML, and sorting by date; update imports.
Dependency pin
package.json
Pin react-native-callkeep dependency to a specific GitHub commit ref.
Misc: tests/import path changes
src/stores/app/core-store.ts, src/stores/app/__tests__/core-store.test.ts, various tests
Adjust import/mock paths to use @/api/config barrel; add cache-manager mocks in contact store tests.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant ContactsScreen
  participant ContactsStore
  participant ContactsAPI
  participant CacheManager
  participant Server

  User->>ContactsScreen: Pull to refresh
  ContactsScreen->>ContactsStore: fetchContacts(true)
  ContactsStore->>ContactsAPI: getAllContacts(true)
  ContactsAPI->>CacheManager: remove('/Contacts/GetAllContacts')
  ContactsAPI->>Server: GET /Contacts/GetAllContacts
  Server-->>ContactsAPI: 200 data
  ContactsAPI-->>ContactsStore: data
  ContactsStore-->>ContactsScreen: update state
  ContactsScreen-->>User: refreshed list
Loading
sequenceDiagram
  participant Caller
  participant ConfigAPI
  participant CachedEndpoint as Cached /Config/GetSystemConfig
  participant Server

  Caller->>ConfigAPI: getSystemConfig()
  ConfigAPI->>CachedEndpoint: GET (ttl=1 day, enabled=false)
  CachedEndpoint->>Server: Request
  Server-->>CachedEndpoint: GetSystemConfigResult
  CachedEndpoint-->>ConfigAPI: response.data
  ConfigAPI-->>Caller: GetSystemConfigResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I tug the list; it wakes and sings,
Cache hops off with feathered wings.
Notes dress plain or dressed in chrome,
Config maps hum, bottom sheets roam.
I thump, I patch, I stitch the flow—hooray! 🐇✨

✨ 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

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

🧹 Nitpick comments (17)
package.json (1)

148-148: Pinned commit is valid; consider tagging a durable release

  • Verified that commit 957193d0716f1c2dfdc18e627cbff0f8a0800971 exists and is accessible upstream.
  • No existing tags reference this commit.
  • For long-term stability, you may fork the repo, tag this commit (e.g. v1.0.0), and update the dependency to point at your tagged release.

Dependency remains:

- "react-native-callkeep": "github:Irfanwani/react-native-callkeep#957193d0716f1c2dfdc18e627cbff0f8a0800971"
+ "react-native-callkeep": "github:<your-fork>/react-native-callkeep#v1.0.0"
src/components/settings/server-url-bottom-sheet.tsx (3)

5-5: Use the UI-layer ScrollView for consistency with gluestack-ui

Per our guidelines, prefer the UI wrapper over raw RN when available to keep styling/behavior consistent.

-import { Platform, ScrollView } from 'react-native';
+import { Platform } from 'react-native';
+import { ScrollView } from '../ui';

Also applies to: 76-76


94-106: Improve UX: submit on keyboard “Done”

Trigger form submission on the keyboard’s “Done” to reduce taps. No behavior change otherwise.

                   <InputField
                     value={value}
                     onChangeText={onChange}
                     placeholder={t('settings.enter_server_url')}
                     autoCapitalize="none"
                     autoCorrect={false}
                     keyboardType="url"
                     textContentType="URL"
                     returnKeyType="done"
                     autoFocus={false}
                     blurOnSubmit={true}
+                    onSubmitEditing={handleSubmit(onFormSubmit)}
                   />

49-66: Normalize the URL before saving (trim/trailing slash)

Users may enter trailing slashes or spaces. Normalize to avoid double slashes like //api/v4.

Example change (outside the shown hunk):

const onFormSubmit = async (data: ServerUrlForm) => {
  try {
    setIsLoading(true);
    const base = data.url.trim().replace(/\/+$/, ''); // remove trailing slashes
    await setUrl(`${base}/api/${Env.API_VERSION}`);
    logger.info({ message: 'Server URL updated successfully', context: { url: base } });
    onClose();
  } catch (error) {
    logger.error({ message: 'Failed to update server URL', context: { error } });
  } finally {
    setIsLoading(false);
  }
};
src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx (2)

116-123: Assert the newly added input props (textContentType, returnKeyType, blurOnSubmit)

These props are part of the recent changes; add expectations to lock behavior.

   it('renders input field with correct keyboard properties', () => {
     render(<ServerUrlBottomSheet {...defaultProps} />);

     const inputField = screen.getByTestId('input-field');
     expect(inputField.props.autoCapitalize).toBe('none');
     expect(inputField.props.autoCorrect).toBe(false);
     expect(inputField.props.keyboardType).toBe('url');
+    expect(inputField.props.textContentType).toBe('URL');
+    expect(inputField.props.returnKeyType).toBe('done');
+    expect(inputField.props.blurOnSubmit).toBe(true);
   });

6-21: Reduce over-mocking of RN Settings unless required by this test

These deep RN Settings mocks add maintenance overhead and can mask real issues. If not needed for this specific test, drop them.

Try removing these blocks locally; if tests still pass, delete them to simplify.

src/app/(app)/__tests__/contacts.test.tsx (1)

292-294: Initial fetch assertion is fine; consider making intent explicit.

Using toHaveBeenCalledWith() is valid to assert no args. Optionally also assert not called with true to make the intent unmistakable.

Suggested addition:

 expect(mockFetchContacts).toHaveBeenCalledTimes(1);
 expect(mockFetchContacts).toHaveBeenCalledWith(); // No force refresh on initial load
+expect(mockFetchContacts).not.toHaveBeenCalledWith(true);
src/stores/contacts/__tests__/store.test.ts (1)

5-5: Remove unused cacheManager mock/import/alias.

These are not referenced; keeping them adds noise and may trigger noUnusedLocals rules.

Apply this diff:

-import { cacheManager } from '@/lib/cache/cache-manager';
@@
-jest.mock('@/lib/cache/cache-manager');
@@
-const mockCacheManager = cacheManager as jest.Mocked<typeof cacheManager>;

Also applies to: 16-16, 20-20

src/api/config/index.ts (3)

7-14: Fix grammar in cache TTL comments

Nit: “1 days” → “1 day” in both comments.

Apply this diff:

-  ttl: 60 * 1000 * 1440, // Cache for 1 days
+  ttl: 60 * 1000 * 1440, // Cache for 1 day
-  ttl: 60 * 1000 * 1440, // Cache for 1 days
+  ttl: 60 * 1000 * 1440, // Cache for 1 day

16-26: Add explicit return types for stronger type safety

Being explicit here improves readability and prevents accidental type drift.

Apply this diff:

-export const getConfig = async (key: string) => {
+export const getConfig = async (key: string): Promise<GetConfigResult> => {
   const response = await getConfigApi.get<GetConfigResult>({
     key: encodeURIComponent(key),
   });
   return response.data;
 };
 
-export const getSystemConfig = async () => {
-  const response = await getSystemConfigApi.get<GetSystemConfigResult>();
-  return response.data;
-};
+export const getSystemConfig = async (): Promise<GetSystemConfigResult> => {
+  const response = await getSystemConfigApi.get<GetSystemConfigResult>();
+  return response.data;
+};

23-26: Consider adding a small unit test for getSystemConfig()

A focused test that stubs getSystemConfigApi.get and asserts the endpoint path, call, and typed return would lock this in.

I can scaffold a Jest test that mocks ../common/cached-client, verifies the GET call to /Config/GetSystemConfig, and asserts the typed data flow. Want me to push that?

src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx (3)

1-4: Use act and assert the refresh actually triggers fetchContacts(true)

Import act so we can simulate the RefreshControl’s onRefresh and verify the force-refresh path unequivocally.

Apply this diff:

-import { render, waitFor } from '@testing-library/react-native';
+import { render, act } from '@testing-library/react-native';

107-133: Strengthen the first test by invoking onRefresh and asserting arguments

Right now it only checks initial call and presence of RefreshControl. Trigger onRefresh to prove fetchContacts(true) is used.

Apply this diff:

-  it('should properly configure pull-to-refresh with force cache refresh', async () => {
-    const mockFetchContacts = jest.fn();
+  it('should properly configure pull-to-refresh with force cache refresh', async () => {
+    const mockFetchContacts = jest.fn().mockResolvedValue(undefined);
@@
-    // Verify that the contacts list has refresh control
-    const flatList = getByTestId('contacts-list');
-    expect(flatList).toBeTruthy();
-    expect(flatList.props.refreshControl).toBeTruthy();
-
-    // The handleRefresh function should be properly configured to call fetchContacts with true
-    // This is tested indirectly through the component structure and our unit tests
-    expect(mockFetchContacts).toHaveBeenCalledWith(); // Initial load without force refresh
+    // Verify that the contacts list has a refresh control we can use
+    const flatList = getByTestId('contacts-list');
+    expect(flatList).toBeTruthy();
+    const refreshControl = flatList.props.refreshControl;
+    expect(refreshControl).toBeTruthy();
+
+    // Trigger pull-to-refresh and assert force refresh is used
+    await act(async () => {
+      await refreshControl.props.onRefresh();
+    });
+    expect(mockFetchContacts).toHaveBeenCalledTimes(2);
+    expect(mockFetchContacts).toHaveBeenNthCalledWith(1);
+    expect(mockFetchContacts).toHaveBeenNthCalledWith(2, true);
   });

149-162: Optionally, execute onRefresh here too to assert the force-refresh call path

This turns the descriptive comment into an executable assertion.

Apply this diff:

     // Verify refresh control is configured
     expect(refreshControl).toBeTruthy();
     expect(refreshControl.props.refreshing).toBe(false);
     expect(typeof refreshControl.props.onRefresh).toBe('function');
 
-    // The handleRefresh function implementation includes:
-    // 1. setRefreshing(true)
-    // 2. await fetchContacts(true) - with force refresh
-    // 3. setRefreshing(false)
-    // This ensures proper state management during refresh
+    // Trigger a refresh and ensure force refresh is used
+    await act(async () => {
+      await refreshControl.props.onRefresh();
+    });
+    expect(mockFetchContacts).toHaveBeenCalledWith(true);
src/components/contacts/contact-notes-list.tsx (3)

45-48: Localize the fallback string and improve HTML detection

Avoid hard-coded English and make the HTML detection more robust so plain text like "2 < 3" isn't misclassified as HTML.

Apply this diff:

-  // Fallback display for empty or plain text notes
-  const isPlainText = !note.Note || !note.Note.includes('<');
-  const noteContent = note.Note || '(No content)';
+  // Fallback display for empty or plain text notes
+  const hasHtmlTags = /<\/?[a-z][\s\S]*>/i.test(note.Note ?? '');
+  const isPlainText = !hasHtmlTags;
+  const noteContent = note.Note?.trim() || t('contacts.noContent');

87-87: Avoid nested scrolling: support dynamic WebView height

Fixed height makes long notes awkward to read and creates nested scroll areas. Auto-size the WebView to its content.

Apply this diff:

-              style={styles.webView}
+              style={[styles.webView, { height: webViewHeight }]}

Add minimal auto-height plumbing in ContactNoteCard:

// state
const [webViewHeight, setWebViewHeight] = React.useState<number>(200);

// in the WebView props
injectedJavaScript={`
  (function() {
    var height = Math.max(
      document.documentElement.scrollHeight || 0,
      document.body.scrollHeight || 0
    );
    window.ReactNativeWebView.postMessage(String(height));
  })();
  true;
`}
onMessage={(e) => {
  const h = Number(e.nativeEvent.data);
  if (!Number.isNaN(h)) {
    // clamp to avoid pathological sizes
    setWebViewHeight(Math.min(Math.max(h, 200), 1000));
  }
}}

288-292: Replace fixed height with minHeight to play well with dynamic sizing

If you adopt auto-height, avoid hard-coding height in the StyleSheet. Keep a minimum for short content.

Apply this diff:

   webView: {
-    height: 200, // Fixed height with scroll capability
+    // Height is controlled dynamically via state; keep a minimum for short content
+    minHeight: 200,
     backgroundColor: 'transparent',
     width: '100%',
   },
📜 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 1f4fb1e and 296109a.

📒 Files selected for processing (16)
  • docs/contacts-pull-to-refresh-implementation.md (1 hunks)
  • package.json (1 hunks)
  • src/api/config/index.ts (2 hunks)
  • src/api/contacts/contacts.ts (2 hunks)
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx (1 hunks)
  • src/app/(app)/__tests__/contacts.test.tsx (1 hunks)
  • src/app/(app)/contacts.tsx (1 hunks)
  • src/components/contacts/contact-notes-list.tsx (5 hunks)
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx (1 hunks)
  • src/components/settings/server-url-bottom-sheet.tsx (2 hunks)
  • src/models/v4/configs/getSystemConfigResult.ts (1 hunks)
  • src/models/v4/configs/getSystemConfigResultData.ts (1 hunks)
  • src/stores/app/__tests__/core-store.test.ts (2 hunks)
  • src/stores/app/core-store.ts (1 hunks)
  • src/stores/contacts/__tests__/store.test.ts (3 hunks)
  • src/stores/contacts/store.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/app/(app)/contacts.tsx
  • src/models/v4/configs/getSystemConfigResult.ts
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/stores/app/core-store.ts
  • src/models/v4/configs/getSystemConfigResultData.ts
  • src/api/contacts/contacts.ts
  • src/app/(app)/__tests__/contacts.test.tsx
  • src/stores/contacts/__tests__/store.test.ts
  • src/api/config/index.ts
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx
  • src/stores/app/__tests__/core-store.test.ts
  • src/components/settings/server-url-bottom-sheet.tsx
  • src/stores/contacts/store.ts
  • src/components/contacts/contact-notes-list.tsx
**/*.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/app/(app)/contacts.tsx
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/app/(app)/__tests__/contacts.test.tsx
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx
  • src/components/settings/server-url-bottom-sheet.tsx
  • src/components/contacts/contact-notes-list.tsx
**/*

📄 CodeRabbit Inference Engine (.cursorrules)

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

Files:

  • src/app/(app)/contacts.tsx
  • src/models/v4/configs/getSystemConfigResult.ts
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/stores/app/core-store.ts
  • src/models/v4/configs/getSystemConfigResultData.ts
  • src/api/contacts/contacts.ts
  • src/app/(app)/__tests__/contacts.test.tsx
  • src/stores/contacts/__tests__/store.test.ts
  • src/api/config/index.ts
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx
  • src/stores/app/__tests__/core-store.test.ts
  • src/components/settings/server-url-bottom-sheet.tsx
  • src/stores/contacts/store.ts
  • package.json
  • docs/contacts-pull-to-refresh-implementation.md
  • src/components/contacts/contact-notes-list.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/app/(app)/contacts.tsx
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/app/(app)/__tests__/contacts.test.tsx
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx
  • src/components/settings/server-url-bottom-sheet.tsx
  • src/components/contacts/contact-notes-list.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__/server-url-bottom-sheet-simple.test.tsx
  • src/app/(app)/__tests__/contacts.test.tsx
  • src/stores/contacts/__tests__/store.test.ts
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx
  • src/stores/app/__tests__/core-store.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__/server-url-bottom-sheet-simple.test.tsx
  • src/app/(app)/__tests__/contacts.test.tsx
  • src/stores/contacts/__tests__/store.test.ts
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx
  • src/stores/app/__tests__/core-store.test.ts
**/*.{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__/server-url-bottom-sheet-simple.test.tsx
  • src/app/(app)/__tests__/contacts.test.tsx
  • src/stores/contacts/__tests__/store.test.ts
  • src/app/(app)/__tests__/contacts-pull-to-refresh.integration.test.tsx
  • src/stores/app/__tests__/core-store.test.ts
🧠 Learnings (3)
📚 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__/server-url-bottom-sheet-simple.test.tsx
  • src/stores/app/__tests__/core-store.test.ts
📚 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 jest.config.@(js|ts) : Provide Jest configuration

Applied to files:

  • src/stores/app/__tests__/core-store.test.ts
📚 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/stores/app/__tests__/core-store.test.ts
🧬 Code Graph Analysis (6)
src/models/v4/configs/getSystemConfigResult.ts (1)
src/models/v4/configs/getSystemConfigResultData.ts (1)
  • GetSystemConfigResultData (1-3)
src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx (1)
src/components/settings/server-url-bottom-sheet.tsx (1)
  • ServerUrlBottomSheet (30-134)
src/app/(app)/__tests__/contacts.test.tsx (2)
src/stores/contacts/store.ts (1)
  • useContactsStore (25-76)
src/app/(app)/contacts.tsx (1)
  • Contacts (17-98)
src/stores/contacts/__tests__/store.test.ts (2)
src/api/contacts/contactNotes.ts (1)
  • getContactNotes (7-12)
src/stores/contacts/store.ts (1)
  • useContactsStore (25-76)
src/api/config/index.ts (2)
src/models/v4/configs/getConfigResult.ts (1)
  • GetConfigResult (4-6)
src/models/v4/configs/getSystemConfigResult.ts (1)
  • GetSystemConfigResult (4-6)
src/components/settings/server-url-bottom-sheet.tsx (3)
src/components/ui/index.tsx (1)
  • ScrollView (18-18)
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)
🪛 LanguageTool
docs/contacts-pull-to-refresh-implementation.md

[grammar] ~8-~8: There might be a mistake here.
Context: ...ncement (src/api/contacts/contacts.ts) - Added cache invalidation support: Modi...

(QB_NEW_EN)


[grammar] ~25-~25: There might be a mistake here.
Context: ...ncement (src/stores/contacts/store.ts) - Updated interface: Modified `fetchCont...

(QB_NEW_EN)


[grammar] ~49-~49: There might be a mistake here.
Context: ...hancement (src/app/(app)/contacts.tsx) - Pull-to-refresh improvement: Updated `...

(QB_NEW_EN)


[grammar] ~64-~64: There might be a mistake here.
Context: ...tores/contacts/tests/store.test.ts`) - Force refresh testing: Added test to v...

(QB_NEW_EN)


[grammar] ~69-~69: There might be a mistake here.
Context: .../app/(app)/tests/contacts.test.tsx`) - Refresh functionality: Added test to v...

(QB_NEW_EN)


[grammar] ~74-~74: There might be a mistake here.
Context: ...s-pull-to-refresh.integration.test.tsx`) - End-to-end verification: Created compr...

(QB_NEW_EN)


[grammar] ~81-~81: There might be a mistake here.
Context: ...res ### ✅ Pull-to-Refresh Functionality - Already implemented: The contacts page...

(QB_NEW_EN)


[grammar] ~86-~86: There might be a mistake here.
Context: ...emains unchanged ### ✅ Cache Management - Smart caching: Normal loads use cache ...

(QB_NEW_EN)


[grammar] ~91-~91: There might be a mistake here.
Context: ...ing fresh requests ### ✅ Loading States - Initial load: Shows full loading scree...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...es for both scenarios ### ✅ Type Safety - TypeScript support: Full type safety m...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...maintained throughout the implementation - Interface consistency: Proper interfac...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...rface definitions for all new parameters - Backward compatibility: Existing code ...

(QB_NEW_EN)


[grammar] ~103-~103: There might be a mistake here.
Context: ...tacts Store Tests: ✅ All Passing (15/15) - Initial state management - Fetch contact...

(QB_NEW_EN)


[grammar] ~104-~104: There might be a mistake here.
Context: ...ssing (15/15) - Initial state management - Fetch contacts (default and force refres...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...tch contacts (default and force refresh) - Error handling - Loading states - Contac...

(QB_NEW_EN)


[grammar] ~106-~106: There might be a mistake here.
Context: ...ault and force refresh) - Error handling - Loading states - Contact notes functiona...

(QB_NEW_EN)


[grammar] ~107-~107: There might be a mistake here.
Context: ...fresh) - Error handling - Loading states - Contact notes functionality - Search and...

(QB_NEW_EN)


[grammar] ~108-~108: There might be a mistake here.
Context: ...ing states - Contact notes functionality - Search and selection features ### Conta...

(QB_NEW_EN)


[grammar] ~111-~111: There might be a mistake here.
Context: ...ntacts Page Tests: ✅ All Passing (11/11) - Component rendering - Loading states - S...

(QB_NEW_EN)


[grammar] ~112-~112: There might be a mistake here.
Context: ...ll Passing (11/11) - Component rendering - Loading states - Search functionality - ...

(QB_NEW_EN)


[grammar] ~113-~113: There might be a mistake here.
Context: ...) - Component rendering - Loading states - Search functionality - Contact selection...

(QB_NEW_EN)


[grammar] ~114-~114: There might be a mistake here.
Context: ... - Loading states - Search functionality - Contact selection - Pull-to-refresh conf...

(QB_NEW_EN)


[grammar] ~115-~115: There might be a mistake here.
Context: ...Search functionality - Contact selection - Pull-to-refresh configuration - Force re...

(QB_NEW_EN)


[grammar] ~116-~116: There might be a mistake here.
Context: ...election - Pull-to-refresh configuration - Force refresh parameter verification ##...

(QB_NEW_EN)


[grammar] ~119-~119: There might be a mistake here.
Context: ...# Integration Tests: ✅ All Passing (3/3) - Pull-to-refresh configuration - Refresh ...

(QB_NEW_EN)


[grammar] ~120-~120: There might be a mistake here.
Context: ...ng (3/3) - Pull-to-refresh configuration - Refresh state management - Loading state...

(QB_NEW_EN)


[grammar] ~121-~121: There might be a mistake here.
Context: ...configuration - Refresh state management - Loading state differentiation ### All C...

(QB_NEW_EN)


[grammar] ~124-~124: There might be a mistake here.
Context: ...act-Related Tests: ✅ All Passing (66/66) - ContactCard component tests - ContactDet...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...ng (66/66) - ContactCard component tests - ContactDetailsSheet component tests - Co...

(QB_NEW_EN)


[grammar] ~126-~126: There might be a mistake here.
Context: ...ts - ContactDetailsSheet component tests - ContactNotesList component tests - Store...

(QB_NEW_EN)


[grammar] ~127-~127: There might be a mistake here.
Context: ...tests - ContactNotesList component tests - Store integration tests - Page component...

(QB_NEW_EN)


[grammar] ~128-~128: There might be a mistake here.
Context: ...omponent tests - Store integration tests - Page component tests ## Implementation ...

(QB_NEW_EN)


[grammar] ~133-~133: There might be a mistake here.
Context: ...plementation Notes ### Caching Strategy - Default behavior: Uses cached data for...

(QB_NEW_EN)


[grammar] ~139-~139: There might be a mistake here.
Context: ... regular navigation ### User Experience - Seamless transition: No breaking chang...

(QB_NEW_EN)


[grammar] ~140-~140: There might be a mistake here.
Context: ...eaking changes to existing functionality - Visual feedback: Pull-to-refresh indic...

(QB_NEW_EN)


[grammar] ~141-~141: There might be a mistake here.
Context: ...to-refresh indicator shows refresh state - Error handling: Graceful error handlin...

(QB_NEW_EN)


[grammar] ~142-~142: There might be a mistake here.
Context: ...error handling during refresh operations - Data freshness: Guarantees fresh data ...

(QB_NEW_EN)


[grammar] ~145-~145: There might be a mistake here.
Context: ...explicitly requests it ### Code Quality - Clean implementation: Minimal changes ...

(QB_NEW_EN)


[grammar] ~146-~146: There might be a mistake here.
Context: ...n**: Minimal changes with maximum impact - Test coverage: Comprehensive test cove...

(QB_NEW_EN)


[grammar] ~147-~147: There might be a mistake here.
Context: ...sive test coverage for new functionality - Type safety: Full TypeScript support m...

(QB_NEW_EN)


[grammar] ~148-~148: There might be a mistake here.
Context: ...ty**: Full TypeScript support maintained - Documentation: Clear code comments and...

(QB_NEW_EN)

🔇 Additional comments (12)
src/api/contacts/contacts.ts (1)

22-27: Remove redundant await and add explicit return type

Cache invalidation via cacheManager.remove() is synchronous (returns void), so awaiting it has no effect. However, specifying an explicit return type improves type safety.

• File: src/api/contacts/contacts.ts
• Function: getAllContacts

Suggested diff:

-export const getAllContacts = async (forceRefresh: boolean = false) => {
+export const getAllContacts = async (forceRefresh: boolean = false): Promise<ContactsResult> => {
   if (forceRefresh) {
     // Clear cache before making the request
-    cacheManager.remove('/Contacts/GetAllContacts');
+    cacheManager.remove('/Contacts/GetAllContacts');
   }

Optional: extract the cache key string ('/Contacts/GetAllContacts') into a shared constant to prevent drift between calls to remove() and your createCachedApiEndpoint.

Likely an incorrect or invalid review comment.

src/components/settings/server-url-bottom-sheet.tsx (1)

69-69: Confirm snapPoints Prop Support in UIActionsheet

I’ve checked our UIActionsheet implementation in src/components/ui/actionsheet/index.tsx and don’t see any explicit handling or forwarding of a snapPoints prop. Please verify that the createActionsheet component from @gluestack-ui/actionsheet actually accepts and uses snapPoints (check its TypeScript definitions or documentation). If it doesn’t, you’ll need to:

  • Extend our wrapper to forward snapPoints to the underlying native component
  • Or remove the prop from server-url-bottom-sheet.tsx to avoid silent failures

Location:

  • src/components/settings/server-url-bottom-sheet.tsx, line 69:
    <Actionsheet isOpen={isOpen} onClose={onClose} snapPoints={[80]}>
src/stores/app/core-store.ts (1)

6-6: LGTM: import consolidated to the config barrel

Using the barrel export improves ergonomics and keeps API boundaries clean. No functional changes to fetchConfig flow.

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

64-65: Barrel import alignment LGTM.

Importing getConfig from '@/api/config' matches the mock and the updated app store implementation.


5-7: Mock path update validated — no stale imports found

I ran the ripgrep scan for any remaining @/api/config/config imports or mocks and found none.

src/stores/contacts/__tests__/store.test.ts (1)

123-127: Store fetch tests cover both default and force-refresh paths.

Good assertions: default path calls getAllContacts(false) and force path calls getAllContacts(true), with state updates verified.

Also applies to: 129-140

src/stores/contacts/store.ts (2)

18-18: Public API updated to support optional force refresh — LGTM.

Signature aligns with component usage and tests.


35-39: Flag propagation to API is correct; loading and error handling preserved.

This ensures UI can bypass cache on pull-to-refresh while maintaining existing state semantics.

src/models/v4/configs/getSystemConfigResult.ts (1)

4-6: Model looks consistent and correct

Matches the existing v4 config result pattern (extends BaseV4Request, Data initialized). No issues spotted.

src/models/v4/configs/getSystemConfigResultData.ts (1)

1-12: LGTM: Data shapes align with API needs

Simple DTOs with sensible defaults; consistent with other v4 model patterns (PascalCase props).

docs/contacts-pull-to-refresh-implementation.md (1)

1-156: Documentation is comprehensive and aligned with the implementation

Clear overview, rationale, and test coverage summary. Nicely ties API/store/component layers together.

src/components/contacts/contact-notes-list.tsx (1)

5-6: Named import for WebView looks correct

Switching to the named export from react-native-webview aligns with current typings and common usage.

Comment on lines +175 to +177
</head>
<body>${noteContent}</body>
</html>
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Sanitize HTML before injecting it into WebView

Raw server-provided HTML is injected directly. Even with JS disabled, unsanitized HTML can still cause rendering quirks; with JS enabled, it’s a full XSS in the WebView context. Sanitize before injection.

Confirm whether backend already sanitizes these notes. If not, sanitize on the client or server.

Apply this diff:

-                    <body>${noteContent}</body>
+                    <body>${sanitizedHtml}</body>

Add a sanitization step in ContactNoteCard:

// Add near the top of ContactNoteCard
import xss from 'xss';

// Inside ContactNoteCard:
const sanitizedHtml = React.useMemo(() => xss(noteContent), [noteContent]);

Note: xss is a small, DOM-less sanitizer suitable for React Native. Alternatively, sanitize on the server to avoid adding a dependency.


🏁 Script executed:

#!/bin/bash
# Show context where `noteContent` is injected into the WebView template
rg -n -C3 'noteContent' -g 'src/components/contacts/contact-notes-list.tsx' || true

# Search for any existing HTML-sanitization logic in the frontend
rg -n 'sanitize' -g 'src' || true
rg -n 'xss' -g 'src' || true

Length of output: 2198


Sanitize HTML before injecting into the WebView
We didn’t find any existing sanitization in the frontend, and raw noteContent is being dropped directly into the <body>…</body> of the WebView’s HTML. Even if JavaScript is disabled, malformed or malicious markup can still break layout or be abused once JS is re-enabled.

Please apply the following mandatory refactor:

  • Confirm whether your backend cleans or strips dangerous HTML from note.Note. If it does not, add sanitization on the server or—preferably for defense-in-depth—on the client as well.

  • In src/components/contacts/contact-notes-list.tsx, replace the direct injection of noteContent with a sanitized variable. For example:

    -                    <body>${noteContent}</body>
    +                    <body>${sanitizedHtml}</body>
  • Near the top of ContactNoteCard, add:

    import xss from 'xss';
    // …
    const sanitizedHtml = React.useMemo(() => xss(noteContent), [noteContent]);
  • Ensure your WebView props disable JavaScript if you don’t need it (javaScriptEnabled={false}), but do not rely on that alone for safety.

By sanitizing here (or on the server), you prevent any embedded scripts or malformed tags from causing XSS or layout issues.

🤖 Prompt for AI Agents
In src/components/contacts/contact-notes-list.tsx around lines 175-177,
noteContent is being injected raw into the WebView HTML body which risks XSS or
layout breakage; confirm whether the backend already strips dangerous HTML and
if not (or for defense-in-depth) sanitize on the client by importing a sanitizer
(e.g., xss), creating a memoized sanitizedHtml = React.useMemo(() =>
xss(noteContent), [noteContent]), and use sanitizedHtml in the generated HTML
instead of noteContent; additionally ensure the WebView props explicitly disable
JavaScript (javaScriptEnabled={false}) unless needed.

@ucswift
Copy link
Member Author

ucswift commented Aug 17, 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 498d3b4 into master Aug 17, 2025
4 of 7 checks passed
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: 1

♻️ Duplicate comments (2)
src/app/(app)/contacts.tsx (1)

37-41: Spinner reset fixed with try/finally — nice follow-through.

This addresses the earlier feedback about the refresh spinner getting stuck on errors. Looks good.

src/components/contacts/contact-notes-list.tsx (1)

45-48: Sanitize HTML before injecting; internationalize the plain-text fallback

Raw note HTML is still injected into WebView. Even with JS off, unsanitized markup is risky (and JS may be toggled later). Also, the literal “(No content)” should be localized.

Apply these diffs:

-  // Fallback display for empty or plain text notes
-  const isPlainText = !note.Note || !note.Note.includes('<');
-  const noteContent = note.Note || '(No content)';
+  // Fallback display for empty or plain text notes
+  const isPlainText = !note.Note || !/<[^>]+>/.test(note.Note);
+  const noteContent = note.Note || t('contacts.noContent');
+  // Sanitize HTML defensively before injecting into WebView
+  const sanitizedHtml = React.useMemo(() => xss(noteContent), [noteContent]);
-                    <body>${noteContent}</body>
+                    <body>${sanitizedHtml}</body>

Add this import (outside the shown range):

import xss from 'xss';

If you prefer server-side sanitization, keep the client-side sanitizer for defense-in-depth. I can wire this end-to-end (including adding the i18n key) if helpful.

Also applies to: 196-197

🧹 Nitpick comments (11)
src/app/(app)/contacts.tsx (4)

35-42: Optionally capture and instrument refresh errors (analytics/toast).

Consider catching the error to track failures (and optionally notify the user). This keeps the UX transparent and helps with diagnostics.

   const handleRefresh = React.useCallback(async () => {
     setRefreshing(true);
     try {
-      await fetchContacts(true); // Force refresh to bypass cache
-    } finally {
+      await fetchContacts(true); // Force refresh to bypass cache
+    } catch (err) {
+      trackEvent('contacts_refresh_failed', {
+        message: err instanceof Error ? err.message : String(err),
+      });
+      // TODO: Optionally show a toast/snackbar here
+    } finally {
       setRefreshing(false);
     }
-  }, [fetchContacts]);
+  }, [fetchContacts, trackEvent]);

84-92: FlatList perf: avoid inline renderItem and add virtualization props.

Inline renderItem creates a new function per render and can trigger unnecessary re-renders. Also, enabling virtualization props improves scroll performance on large lists.

           <FlatList
             testID="contacts-list"
             data={filteredContacts}
-            keyExtractor={(item) => item.ContactId}
-            renderItem={({ item }) => <ContactCard contact={item} onPress={selectContact} />}
+            keyExtractor={keyExtractor}
+            renderItem={renderItem}
             showsVerticalScrollIndicator={false}
             contentContainerStyle={{ paddingBottom: 100 }}
+            removeClippedSubviews
+            maxToRenderPerBatch={10}
+            windowSize={7}
             refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
           />

Place this near the other hooks (above the return):

// Add to imports (top of file):
// import { ListRenderItem } from 'react-native';

type Contact = {
  ContactId: string | number;
  FirstName?: string;
  LastName?: string;
  Email?: string;
  CompanyName?: string;
  OtherName?: string;
};

const keyExtractor = React.useCallback((item: Contact) => String(item.ContactId), []);

const renderItem = React.useCallback<ListRenderItem<Contact>>(
  ({ item }) => <ContactCard contact={item} onPress={selectContact} />,
  [selectContact]
);

44-56: Normalize search query once; reuse to reduce repeated trim/compute.

Small readability/perf cleanup and consistent whitespace handling.

   const filteredContacts = React.useMemo(() => {
-    if (!searchQuery.trim()) return contacts;
-
-    const query = searchQuery.toLowerCase();
+    const normalizedQuery = searchQuery.trim().toLowerCase();
+    if (!normalizedQuery) return contacts;
     return contacts.filter(
       (contact) =>
-        (contact.FirstName?.toLowerCase().includes(query) ?? false) ||
-        (contact.LastName?.toLowerCase().includes(query) ?? false) ||
-        (contact.Email?.toLowerCase().includes(query) ?? false) ||
-        (contact.CompanyName?.toLowerCase().includes(query) ?? false) ||
-        (contact.OtherName?.toLowerCase().includes(query) ?? false)
+        (contact.FirstName?.toLowerCase().includes(normalizedQuery) ?? false) ||
+        (contact.LastName?.toLowerCase().includes(normalizedQuery) ?? false) ||
+        (contact.Email?.toLowerCase().includes(normalizedQuery) ?? false) ||
+        (contact.CompanyName?.toLowerCase().includes(normalizedQuery) ?? false) ||
+        (contact.OtherName?.toLowerCase().includes(normalizedQuery) ?? false)
     );
   }, [contacts, searchQuery]);

27-33: Analytics effect fires on every search change; consider firing “rendered” once.

If the intent is to track only the initial render, gate the call so it runs once. You can separately track search changes if desired.

-  // Track when contacts view is rendered
-  React.useEffect(() => {
-    trackEvent('contacts_view_rendered', {
-      contactsCount: contacts.length,
-      hasSearchQuery: searchQuery.length > 0,
-    });
-  }, [trackEvent, contacts.length, searchQuery]);
+  // Track when contacts view is first rendered (once)
+  const renderedRef = React.useRef(false);
+  React.useEffect(() => {
+    if (!renderedRef.current) {
+      renderedRef.current = true;
+      trackEvent('contacts_view_rendered', {
+        contactsCount: contacts.length,
+      });
+    }
+  }, [trackEvent, contacts.length]);
+
+  // Optionally, track search state changes separately
+  // React.useEffect(() => {
+  //   trackEvent('contacts_search_changed', { hasSearchQuery: searchQuery.length > 0 });
+  // }, [trackEvent, searchQuery]);
src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx (4)

32-40: Make react-hook-form handleSubmit behave like real API

Returning a no-op function today is fine, but aligning with RHF’s API will future-proof “Save” button interaction tests.

Apply this diff:

-jest.mock('react-hook-form', () => ({
-  useForm: () => ({
-    control: {},
-    handleSubmit: jest.fn(),
-    setValue: jest.fn(),
-    formState: { errors: {} },
-  }),
-  Controller: ({ render }: any) => render({ field: { onChange: jest.fn(), value: '' } }),
-}));
+jest.mock('react-hook-form', () => ({
+  useForm: () => ({
+    control: {},
+    // Return a function that invokes the provided callback with minimal form data
+    handleSubmit: (fn: any) => () => fn({ url: '' }),
+    setValue: jest.fn(),
+    formState: { errors: {} },
+  }),
+  Controller: ({ render }: any) => render({ field: { onChange: jest.fn(), value: '' } }),
+}));

52-67: Use real RN host components in mocks instead of string element types

Using 'View'/'Text'/'TextInput' strings works in many cases, but referencing the actual RN components reduces renderer edge cases and keeps prop behavior consistent.

Apply this diff:

 const createMockUIComponent = (displayName: string) => ({ children, testID, ...props }: any) => {
   const React = require('react');
-  return React.createElement('View', { testID: testID || displayName.toLowerCase(), ...props }, children);
+  const { View } = require('react-native');
+  return React.createElement(View, { testID: testID || displayName.toLowerCase(), ...props }, children);
 };
 
 const createMockTextComponent = (displayName: string) => ({ children, testID, ...props }: any) => {
   const React = require('react');
-  return React.createElement('Text', { testID: testID || displayName.toLowerCase(), ...props }, children);
+  const { Text } = require('react-native');
+  return React.createElement(Text, { testID: testID || displayName.toLowerCase(), ...props }, children);
 };
 
 const createMockInputComponent = ({ testID, ...props }: any) => {
   const React = require('react');
-  return React.createElement('TextInput', { testID: testID || 'input-field', ...props });
+  const { TextInput } = require('react-native');
+  return React.createElement(TextInput, { testID: testID || 'input-field', ...props });
 };

68-75: Forward props in Actionsheet mock to preserve handlers and attributes

This keeps onClose and other props available for future interaction tests while keeping the conditional rendering behavior.

Apply this diff:

-jest.mock('../../ui/actionsheet', () => ({
-  Actionsheet: ({ children, isOpen }: any) => isOpen ? createMockUIComponent('Actionsheet')({ children }) : null,
+jest.mock('../../ui/actionsheet', () => ({
+  Actionsheet: ({ isOpen, ...props }: any) =>
+    isOpen ? createMockUIComponent('Actionsheet')({ ...props }) : null,
   ActionsheetBackdrop: createMockUIComponent('ActionsheetBackdrop'),
   ActionsheetContent: createMockUIComponent('ActionsheetContent'),
   ActionsheetDragIndicator: createMockUIComponent('ActionsheetDragIndicator'),
   ActionsheetDragIndicatorWrapper: createMockUIComponent('ActionsheetDragIndicatorWrapper'),
 }));

116-123: Expand assertions to cover newly added input props (prevents regressions)

The component sets textContentType, returnKeyType, and blurOnSubmit. Verifying them will better align tests with the UI contract.

Apply this diff:

     const inputField = screen.getByTestId('input-field');
     expect(inputField.props.autoCapitalize).toBe('none');
     expect(inputField.props.autoCorrect).toBe(false);
     expect(inputField.props.keyboardType).toBe('url');
+    expect(inputField.props.textContentType).toBe('URL');
+    expect(inputField.props.returnKeyType).toBe('done');
+    expect(inputField.props.blurOnSubmit).toBe(true);
src/components/contacts/contact-notes-list.tsx (3)

294-301: Prefer FlatList for rendering note cards to enable virtualization

For a potentially unbounded list, FlatList avoids mounting all WebViews at once and aligns with performance guidelines.

Apply this diff:

-  return (
-    <VStack space="md" className="flex-1 p-4">
-      {sortedNotes.map((note) => (
-        <ContactNoteCard key={note.ContactNoteId} note={note} />
-      ))}
-    </VStack>
-  );
+  return (
+    <FlatList
+      contentContainerStyle={{ padding: 16 }}
+      data={sortedNotes}
+      keyExtractor={(item) => String(item.ContactNoteId)}
+      renderItem={({ item }) => <ContactNoteCard note={item} />}
+      initialNumToRender={5}
+      windowSize={7}
+      maxToRenderPerBatch={10}
+      updateCellsBatchingPeriod={50}
+      removeClippedSubviews
+    />
+  );

And add this import (outside the shown range):

import { FlatList } from 'react-native';

35-35: Remove unused backgroundColor constant

This variable isn’t used; drop it to keep things tidy (or apply it where intended).

-  const backgroundColor = colorScheme === 'dark' ? '#374151' : '#F9FAFB';

30-31: Use a single source of truth for expiration logic and display

ContactNoteResultData exposes both ExpiresOnUtc (Date) and ExpiresOn (string ISO). Today you use ExpiresOnUtc to compute isExpired but fall back to formatDate(note.ExpiresOn) for the UI, which can lead to subtle timezone or parsing mismatches. To eliminate any confusion, reference the same field in both places—preferably the already-parsed ExpiresOnUtc.

• In src/components/contacts/contact-notes-list.tsx:
– Replace the note.ExpiresOn guard and display with note.ExpiresOnUtc.
– Update the conditional and formatting at lines 210–215 to use formatDate(note.ExpiresOnUtc).

Example diff:

- ) : note.ExpiresOn ? (
+ ) : note.ExpiresOnUtc ? (
   <HStack space="xs" className="items-center">
     <ClockIcon size={14} color="#6b7280" />
-    <Text className="text-xs text-gray-500 dark:text-gray-400">
-      {t('contacts.expires')}: {formatDate(note.ExpiresOn)}
+    <Text className="text-xs text-gray-500 dark:text-gray-400">
+      {t('contacts.expires')}: {formatDate(note.ExpiresOnUtc)}
     </Text>
   </HStack>

Also ensure the note.ExpiresOnUtc guard replaces any other fallbacks in this file (e.g. at lines 30–31 and 210–215). If the UX requires a localized display string, you can still derive it solely from ExpiresOnUtc—this keeps logic and presentation aligned.

📜 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 296109a and 6db751e.

📒 Files selected for processing (5)
  • src/api/config/index.ts (2 hunks)
  • src/app/(app)/__tests__/contacts.test.tsx (2 hunks)
  • src/app/(app)/contacts.tsx (1 hunks)
  • src/components/contacts/contact-notes-list.tsx (5 hunks)
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/api/config/index.ts
  • src/app/(app)/tests/contacts.test.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 (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/app/(app)/contacts.tsx
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/components/contacts/contact-notes-list.tsx
**/*.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/app/(app)/contacts.tsx
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/components/contacts/contact-notes-list.tsx
**/*

📄 CodeRabbit Inference Engine (.cursorrules)

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

Files:

  • src/app/(app)/contacts.tsx
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/components/contacts/contact-notes-list.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/app/(app)/contacts.tsx
  • src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
  • src/components/contacts/contact-notes-list.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__/server-url-bottom-sheet-simple.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__/server-url-bottom-sheet-simple.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__/server-url-bottom-sheet-simple.test.tsx
🧠 Learnings (2)
📚 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__/server-url-bottom-sheet-simple.test.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 : Use React Navigation for navigation and deep linking with best practices

Applied to files:

  • src/components/contacts/contact-notes-list.tsx
🧬 Code Graph Analysis (1)
src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx (1)
src/components/settings/server-url-bottom-sheet.tsx (1)
  • ServerUrlBottomSheet (30-134)
🔇 Additional comments (4)
src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx (3)

23-30: Partial mock of react-native is correct and avoids brittle full-module stubs

This addresses prior feedback by preserving real exports while overriding only Platform.OS. Good call.


106-114: Visibility tests are focused and stable

Asserting presence/absence via the Actionsheet testID is straightforward and resilient given the current mock setup.


15-21: server-url-bottom-sheet-simple.test.tsx: reconsider RN Settings mocks

I ran a code search and found that these deep-internal Settings mocks only live in this one test file—no other TS/TSX in the repo imports react-native/Libraries/Settings/Settings.ios or NativeSettingsManager. If your tests still pass without them, it’s safer to drop these mocks to reduce future maintenance. Otherwise, please leave them in place with a brief comment explaining which warning or error they silence.

• File needing attention:

  • src/components/settings/tests/server-url-bottom-sheet-simple.test.tsx
    • Lines to review/remove:
- jest.mock('react-native/Libraries/Settings/Settings.ios', () => ({}));
- jest.mock('react-native/Libraries/Settings/NativeSettingsManager', () => ({
-   getConstants: () => ({}),
-   get: jest.fn(),
-   set: jest.fn(),
- }));

• Next steps:

  1. Comment out or delete these mocks and re-run the test suite.
  2. If you encounter failures or warnings, restore them with a short comment (e.g. “Silences RN Settings warning in iOS”).
  3. Otherwise, commit the removal to simplify the test.
src/components/contacts/contact-notes-list.tsx (1)

96-98: Good hardening: JS and DOM storage disabled in WebView

Disabling JavaScript and DOM storage by default is the right default posture for semi/untrusted content.

Comment on lines +89 to +116
originWhitelist={['about:']}
scrollEnabled={true}
showsVerticalScrollIndicator={true}
showsHorizontalScrollIndicator={false}
androidLayerType="software"
// Security: Disable JavaScript and DOM storage by default
// Only re-enable for pre-sanitized, trusted content that requires it
javaScriptEnabled={false}
domStorageEnabled={false}
startInLoadingState={false}
mixedContentMode="compatibility"
// Security: Handle navigation to prevent in-WebView navigation and open external links safely
onShouldStartLoadWithRequest={(request) => {
// Allow initial load of our HTML content
if (request.url.startsWith('about:') || request.url.startsWith('data:')) {
return true;
}

// For any external links, open in system browser instead
Linking.openURL(request.url);
return false;
}}
onNavigationStateChange={(navState) => {
// Additional protection: if navigation occurs to external URL, open in system browser
if (navState.url && !navState.url.startsWith('about:') && !navState.url.startsWith('data:')) {
Linking.openURL(navState.url);
}
}}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tighten WebView origin policy and external link handling; remove redundant navigation handler

  • Use about:blank precisely (not about:) to match the inline HTML load.
  • Drop mixedContentMode="compatibility" (unnecessary risk surface for local content).
  • Gate external URLs to http(s)/tel/mailto, and guard Linking.openURL errors.
  • Rely on onShouldStartLoadWithRequest; remove onNavigationStateChange to avoid duplicate opens. Also set setSupportMultipleWindows={false} to prevent target=_blank popups.

Apply this diff:

-              originWhitelist={['about:']}
+              originWhitelist={['about:blank']}
               scrollEnabled={true}
               showsVerticalScrollIndicator={true}
               showsHorizontalScrollIndicator={false}
               androidLayerType="software"
               // Security: Disable JavaScript and DOM storage by default
               // Only re-enable for pre-sanitized, trusted content that requires it
               javaScriptEnabled={false}
               domStorageEnabled={false}
               startInLoadingState={false}
-              mixedContentMode="compatibility"
+              setSupportMultipleWindows={false}
               // Security: Handle navigation to prevent in-WebView navigation and open external links safely
-              onShouldStartLoadWithRequest={(request) => {
-                // Allow initial load of our HTML content
-                if (request.url.startsWith('about:') || request.url.startsWith('data:')) {
-                  return true;
-                }
-
-                // For any external links, open in system browser instead
-                Linking.openURL(request.url);
-                return false;
-              }}
-              onNavigationStateChange={(navState) => {
-                // Additional protection: if navigation occurs to external URL, open in system browser
-                if (navState.url && !navState.url.startsWith('about:') && !navState.url.startsWith('data:')) {
-                  Linking.openURL(navState.url);
-                }
-              }}
+              onShouldStartLoadWithRequest={(request) => {
+                const url = request?.url ?? '';
+                // Allow initial load and data URIs
+                if (url.startsWith('about:') || url.startsWith('data:')) return true;
+                // Only allow safe external schemes and open in system handlers
+                const isHttp = url.startsWith('http://') || url.startsWith('https://');
+                const isTel = url.startsWith('tel:');
+                const isMail = url.startsWith('mailto:');
+                if (isHttp || isTel || isMail) {
+                  Linking.openURL(url).catch(() => {});
+                }
+                return false;
+              }}
📝 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
originWhitelist={['about:']}
scrollEnabled={true}
showsVerticalScrollIndicator={true}
showsHorizontalScrollIndicator={false}
androidLayerType="software"
// Security: Disable JavaScript and DOM storage by default
// Only re-enable for pre-sanitized, trusted content that requires it
javaScriptEnabled={false}
domStorageEnabled={false}
startInLoadingState={false}
mixedContentMode="compatibility"
// Security: Handle navigation to prevent in-WebView navigation and open external links safely
onShouldStartLoadWithRequest={(request) => {
// Allow initial load of our HTML content
if (request.url.startsWith('about:') || request.url.startsWith('data:')) {
return true;
}
// For any external links, open in system browser instead
Linking.openURL(request.url);
return false;
}}
onNavigationStateChange={(navState) => {
// Additional protection: if navigation occurs to external URL, open in system browser
if (navState.url && !navState.url.startsWith('about:') && !navState.url.startsWith('data:')) {
Linking.openURL(navState.url);
}
}}
originWhitelist={['about:blank']}
scrollEnabled={true}
showsVerticalScrollIndicator={true}
showsHorizontalScrollIndicator={false}
androidLayerType="software"
// Security: Disable JavaScript and DOM storage by default
// Only re-enable for pre-sanitized, trusted content that requires it
javaScriptEnabled={false}
domStorageEnabled={false}
startInLoadingState={false}
setSupportMultipleWindows={false}
// Security: Handle navigation to prevent in-WebView navigation and open external links safely
onShouldStartLoadWithRequest={(request) => {
const url = request?.url ?? '';
// Allow initial load and data URIs
if (url.startsWith('about:') || url.startsWith('data:')) {
return true;
}
// Only allow safe external schemes and open in system handlers
const isHttp = url.startsWith('http://') || url.startsWith('https://');
const isTel = url.startsWith('tel:');
const isMail = url.startsWith('mailto:');
if (isHttp || isTel || isMail) {
Linking.openURL(url).catch(() => {});
}
return false;
}}
🤖 Prompt for AI Agents
In src/components/contacts/contact-notes-list.tsx around lines 89 to 116,
tighten the WebView security by changing the origin whitelist to 'about:blank'
(to match the inline HTML), remove the mixedContentMode prop, add
setSupportMultipleWindows={false} to disable popups, gate external link handling
in onShouldStartLoadWithRequest to only allow http(s), tel, and mailto schemes
(reject everything else), and wrap Linking.openURL with a safe check/guard
(e.g., Linking.canOpenURL or try/catch) to avoid unhandled errors; also remove
the onNavigationStateChange handler to prevent duplicate external opens and rely
solely on onShouldStartLoadWithRequest.

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