Skip to content

Conversation

@Devasy
Copy link
Owner

@Devasy Devasy commented Nov 27, 2025

Introduce a revamped Friends page with group breakdowns and loading states, improve group management functionalities including member management and settings, and create a new Profile page for user account management. Update API services to support new profile and group management features, and integrate Firebase for Google authentication. Enhance UI components for better user experience.

Summary by CodeRabbit

  • New Features

    • Profile page with editable name/avatar, image preview/upload, edit modal, and logout
    • Google Sign‑In flow with server login integration
    • Rich Group Details: settings, invite/share, member management, settle-up, add/edit expenses and payments
    • Group search and immersive Groups UI
  • Improvements

    • Avatars now show user images when available; client-side upload validation and previews
    • Friends and Dashboard enhanced with animations, theme-aware styling, and improved UX
  • Chores

    • Added Firebase integration for Google authentication and related sign-in flow

✏️ Tip: You can customize this high-level summary in your review settings.

- Refactored Friends component to include group breakdown for each friend.
- Added loading skeletons while fetching friends data.
- Implemented expandable friend rows to show detailed group balances.
- Improved error handling and user feedback during data fetching.

feat: improve GroupDetails with member management and settings tabs

- Added functionality to leave groups and kick members with confirmation prompts.
- Introduced settings tabs for group information, members, and danger actions.
- Enhanced UI for inviting members and managing group settings.

feat: create Profile page for user account management

- Implemented profile editing functionality with image upload and name change.
- Added modal for editing profile details and handling image selection.
- Integrated logout functionality and menu items for account settings.

feat: update API service with new endpoints for profile and group management

- Added API calls for updating user profiles and managing group memberships.
- Included Google login functionality in the authentication service.

chore: add Firebase service for Google authentication

- Set up Firebase configuration and authentication methods for Google sign-in.
@Devasy Devasy requested a review from vrajpatelll as a code owner November 27, 2025 16:20
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 27, 2025

Walkthrough

Adds Firebase Google sign-in flow, new Profile page with edit/avatar upload, new API wrappers (loginWithGoogle, updateProfile, leaveGroup, removeMember), exposes updateUserInContext in AuthContext, and applies theme-aware UI/animation updates across multiple pages and components.

Changes

Cohort / File(s) Change Summary
Auth & Firebase integration
web/pages/Auth.tsx, web/services/firebase.ts, web/services/api.ts, web/package.json, web/contexts/AuthContext.tsx
Add Firebase init and signInWithGoogle popup helper; add firebase dependency; add loginWithGoogle, updateProfile, leaveGroup, removeMember API wrappers; integrate Google sign-in in Auth UI; expose updateUserInContext from AuthContext.
New page: Profile
web/pages/Profile.tsx
Add exported Profile component with themed header, avatar preview/upload, edit profile modal with validation, calls updateProfile, updates local user context, and logout flow.
Pages: Groups / GroupDetails / Friends / Dashboard / Routing
web/pages/Groups.tsx, web/pages/GroupDetails.tsx, web/pages/Friends.tsx, web/pages/Dashboard.tsx, web/App.tsx
Large UI refactors: theme-driven styling and animations, search/filtering, typed data transforms, expanded group/expense/member flows, and replace placeholder profile route with real Profile component.
UI components & layout tweaks
web/components/ui/Button.tsx, web/components/ui/Modal.tsx, web/components/layout/Sidebar.tsx
Minor formatting and styling changes: Button adds font-mono for NEOBRUTALISM, Modal adjusts NEOBRUTALISM rounding and consolidates motion imports, Sidebar conditionally renders image avatars or gradient initials.

Possibly related PRs

Suggested reviewers

  • vrajpatelll

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: it introduces Friends page enhancements, Groups management improvements, and a new Profile page component.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

❤️ Share

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

@netlify
Copy link

netlify bot commented Nov 27, 2025

Deploy Preview for split-but-wiser ready!

Name Link
🔨 Latest commit 6bbb1b2
🔍 Latest deploy log https://app.netlify.com/projects/split-but-wiser/deploys/6928987255cd130008a00eda
😎 Deploy Preview https://deploy-preview-206--split-but-wiser.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link

codecov bot commented Nov 27, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.79%. Comparing base (13ac19d) to head (6bbb1b2).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #206   +/-   ##
=======================================
  Coverage   72.79%   72.79%           
=======================================
  Files          17       17           
  Lines        1669     1669           
  Branches      154      154           
=======================================
  Hits         1215     1215           
  Misses        400      400           
  Partials       54       54           
Components Coverage Δ
Authentication System 75.45% <ø> (ø)
Expense Management 69.88% <ø> (ø)
Group Management 69.49% <ø> (ø)
User Management 97.16% <ø> (ø)
Backend Core 69.41% <ø> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

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

Caution

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

⚠️ Outside diff range comments (6)
web/components/layout/Sidebar.tsx (1)

55-68: Share image URL validation with Profile to avoid drift

The avatar logic (URL/data‑URI check with fallback initial) looks good, but it duplicates the image validation used in Profile.tsx. Consider extracting a small shared helper (e.g., isValidImageUrl) and reusing it here to keep behavior consistent if you ever tighten/relax the validation rules.

web/contexts/AuthContext.tsx (1)

5-57: Normalize user shape in updateUserInContext (align with initAuth / mobile)

initAuth maps id_id, but updateUserInContext (and login) currently store userData as‑is. If the backend returns only id, consumers expecting _id will see inconsistent shapes depending on how the user was loaded/updated.

Suggest normalizing here, similar to the mobile context:

const normalizeUser = (u: any): User | null => {
  if (!u) return null;
  return u._id
    ? u
    : u.id
    ? { ...u, _id: u.id }
    : u;
};

const login = (token: string, userData: User) => {
  localStorage.setItem('access_token', token);
  setUser(normalizeUser(userData));
};

const updateUserInContext = (userData: User) => {
  setUser(normalizeUser(userData));
};

This keeps the user shape stable regardless of load path and matches the mobile implementation.

web/pages/Groups.tsx (4)

48-50: Avoid using any type; use proper typing.

The g: any weakens type safety. Since GroupBalanceSummary is already imported, use it directly.

 const getGroupBalance = (groupId: string): GroupBalanceSummary | undefined => {
-  return balanceSummary?.groupsSummary?.find((g: any) => g.groupId === groupId);
+  return balanceSummary?.groupsSummary?.find((g: GroupBalanceSummary) => g.groupId === groupId);
 };

59-61: Consider replacing alert() with toast notifications.

Using alert() blocks the UI and provides a poor user experience. Consider using a toast/notification system for error feedback.


222-226: Form validation bypassed by direct button onClick.

The required attribute on the Input (line 236) won't prevent submission because the Button's onClick directly calls handleCreateGroup instead of triggering form submission. Either attach onSubmit to the form and use type="submit" on the button, or add manual validation in the handler.

-      <form id="createGroupForm" className="space-y-4">
+      <form id="createGroupForm" className="space-y-4" onSubmit={handleCreateGroup}>

And in the footer, change the button to submit type or keep as is if using form's onSubmit:

-          <Button onClick={handleCreateGroup}>Create Group</Button>
+          <Button type="submit" form="createGroupForm">Create Group</Button>

246-250: Same form validation issue in Join Group modal.

The Join Group form has the same validation bypass issue. The required attribute on the Input won't be enforced.

Apply a similar fix:

-      <form className="space-y-4">
+      <form id="joinGroupForm" className="space-y-4" onSubmit={handleJoinGroup}>
-          <Button onClick={handleJoinGroup}>Join Group</Button>
+          <Button type="submit" form="joinGroupForm">Join Group</Button>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13ac19d and c3d90ec.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • web/App.tsx (2 hunks)
  • web/components/layout/Sidebar.tsx (2 hunks)
  • web/components/ui/Button.tsx (3 hunks)
  • web/components/ui/Modal.tsx (3 hunks)
  • web/contexts/AuthContext.tsx (2 hunks)
  • web/package.json (1 hunks)
  • web/pages/Auth.tsx (2 hunks)
  • web/pages/Dashboard.tsx (4 hunks)
  • web/pages/Friends.tsx (1 hunks)
  • web/pages/GroupDetails.tsx (1 hunks)
  • web/pages/Groups.tsx (6 hunks)
  • web/pages/Profile.tsx (1 hunks)
  • web/services/api.ts (2 hunks)
  • web/services/firebase.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
web/components/layout/Sidebar.tsx (1)
mobile/context/AuthContext.js (1)
  • user (13-13)
web/App.tsx (1)
web/pages/Profile.tsx (1)
  • Profile (13-275)
web/contexts/AuthContext.tsx (2)
web/types.ts (1)
  • User (1-8)
mobile/context/AuthContext.js (4)
  • user (13-13)
  • updateUserInContext (165-173)
  • AuthContext (10-10)
  • AuthContext (10-10)
web/pages/Profile.tsx (8)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (1)
  • updateProfile (50-50)
web/constants.ts (1)
  • THEMES (1-4)
web/types.ts (1)
  • User (1-8)
web/components/ui/Modal.tsx (1)
  • Modal (15-89)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/services/api.ts (1)
mobile/api/groups.js (4)
  • leaveGroup (40-41)
  • leaveGroup (40-41)
  • removeMember (46-47)
  • removeMember (46-47)
web/pages/Groups.tsx (6)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/constants.ts (1)
  • THEMES (1-4)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Skeleton.tsx (1)
  • Skeleton (9-23)
web/components/ui/Modal.tsx (1)
  • Modal (15-89)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/components/ui/Button.tsx (1)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/pages/Dashboard.tsx (2)
web/constants.ts (1)
  • THEMES (1-4)
web/components/ui/Card.tsx (1)
  • Card (12-35)
web/pages/Auth.tsx (6)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/firebase.ts (1)
  • signInWithGoogle (21-26)
web/services/api.ts (2)
  • loginWithGoogle (23-23)
  • login (21-21)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/components/ui/Modal.tsx (1)
web/constants.ts (1)
  • THEMES (1-4)
web/pages/Friends.tsx (3)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (2)
  • getFriendsBalance (49-49)
  • getGroups (27-27)
web/constants.ts (1)
  • THEMES (1-4)
🪛 Biome (2.1.2)
web/pages/Profile.tsx

[error] 127-127: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 127-127: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 237-237: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 237-237: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 182-187: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

web/pages/Auth.tsx

[error] 166-167: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

web/pages/Friends.tsx

[error] 262-265: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

web/pages/GroupDetails.tsx

[error] 578-578: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 587-587: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 648-660: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 648-660: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 740-740: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 753-753: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 884-884: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)


[error] 359-359: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 380-383: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 402-405: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 408-411: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 780-783: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 786-789: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 792-795: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)


[error] 843-847: Provide an explicit type prop for the button element.

The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset

(lint/a11y/useButtonType)

🪛 Gitleaks (8.29.1)
web/services/firebase.ts

[high] 6-6: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)

⏰ 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: preview
🔇 Additional comments (12)
web/package.json (1)

11-19: Firebase dependency aligns with new auth service

Adding firebase to dependencies matches the new Firebase auth module usage and fits the existing tooling stack; no issues from this manifest change alone.

web/components/ui/Button.tsx (1)

10-55: Button theming changes are consistent and non‑breaking

Button still exposes the same props and behavior; the added NEOBRUTALISM styling (font-mono, uppercase, etc.) cleanly composes with existing base and size classes.

web/App.tsx (1)

2-44: Profile route integration looks correct

/profile is wired through ProtectedRoute and uses the shared Layout, consistent with other authenticated pages. Redirect logic for login/signup and the catch‑all route remains coherent.

web/components/ui/Modal.tsx (1)

1-88: Modal theme/animation tweaks are coherent and scoped

The new NEOBRUTALISM animation variant and rounded-none styling integrate cleanly with the existing Modal API, keeping overlay behavior and close controls intact while only altering visuals based on theme.

web/services/api.ts (1)

21-55: New auth/profile/group API wrappers match existing patterns

loginWithGoogle, updateProfile, leaveGroup, and removeMember all follow the established API client pattern and mirror the mobile endpoints, so they should integrate cleanly with the backend and existing interceptors.

web/pages/Auth.tsx (2)

30-54: Good error handling for Google sign-in flow.

The differentiation between popup-closed-by-user, server errors, and general failures provides appropriate user feedback without exposing unnecessary error details.


73-79: Error message extraction may not handle all API response formats.

The error extraction err.response.data?.detail?.[0]?.msg assumes detail is an array with objects containing msg. If the API returns a different format (e.g., string detail), users will see the generic "Authentication failed" message.

Consider a more robust extraction:

       if (err.response) {
-        setError(
-          err.response.data?.detail?.[0]?.msg || 'Authentication failed'
-        );
+        const detail = err.response.data?.detail;
+        const message = typeof detail === 'string'
+          ? detail
+          : Array.isArray(detail) ? detail[0]?.msg : null;
+        setError(message || 'Authentication failed');
       } else {
web/pages/GroupDetails.tsx (1)

188-239: Expense submission validation could silently fail.

The handleExpenseSubmit function returns early with alert() on validation failures but doesn't prevent further execution paths clearly. The early returns are correct, but consider consolidating validation logic for clarity.

The validation logic correctly handles equal vs unequal splits with appropriate user feedback. The filtering of zero-amount splits at line 217 is a good defensive measure.

web/pages/Groups.tsx (4)

1-12: LGTM!

Imports are well-organized and all appear to be utilized in the component.


22-26: LGTM!

Good use of a derived isNeo boolean to simplify theme-based conditionals throughout the component.


80-93: LGTM!

Animation variants are well-structured for staggered list animations.


150-207: LGTM on the AnimatePresence and group card structure.

The conditional rendering with AnimatePresence mode='popLayout' is correctly set up for smooth transitions between loading and loaded states. Theme-aware styling is comprehensive and consistent.

Comment on lines 64 to 66
<h2 className={`text-3xl font-extrabold ${summary && summary.netBalance >= 0 ? (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500') : (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-red-500')}`}>
${summary?.netBalance.toFixed(2)}
</h2>
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting the complex ternary for readability.

The nested ternary for Net Balance color is difficult to parse. Consider extracting to a helper variable.

+  const netBalanceColor = summary && summary.netBalance >= 0
+    ? (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500')
+    : (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-red-500');
+
   return (
     ...
-          <h2 className={`text-3xl font-extrabold ${summary && summary.netBalance >= 0 ? (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500') : (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-red-500')}`}>
+          <h2 className={`text-3xl font-extrabold ${netBalanceColor}`}>
📝 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
<h2 className={`text-3xl font-extrabold ${summary && summary.netBalance >= 0 ? (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500') : (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-red-500')}`}>
${summary?.netBalance.toFixed(2)}
</h2>
const netBalanceColor = summary && summary.netBalance >= 0
? (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500')
: (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-red-500');
return (
...
<h2 className={`text-3xl font-extrabold ${netBalanceColor}`}>
${summary?.netBalance.toFixed(2)}
</h2>
)
🤖 Prompt for AI Agents
In web/pages/Dashboard.tsx around lines 64 to 66, the long nested ternary used
to choose the net balance text color harms readability; extract the logic into a
small, clearly named helper variable (e.g., balanceColorClass) above the JSX
that computes the class by first checking summary?.netBalance >= 0 and then
style === THEMES.NEOBRUTALISM to return one of 'text-black', 'text-emerald-500',
or 'text-red-500' (handle summary undefined safely), and then replace the inline
ternary in the h2 className with that variable so the JSX reads simply
`className={\`text-3xl font-extrabold ${balanceColorClass}\`}`.

Comment on lines +4 to +29
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyC4Ny4BSh3q4fNEVBGyw2u_FvLaxXukB8U",
authDomain: "splitwiser-25e34.firebaseapp.com",
projectId: "splitwiser-25e34",
storageBucket: "splitwiser-25e34.firebasestorage.app",
messagingSenderId: "323312632683",
appId: "1:323312632683:web:eef9ca7acc5c5a89ce422e",
measurementId: "G-SDY9ZRV9V4"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const googleProvider = new GoogleAuthProvider();

// Sign in with Google popup
export const signInWithGoogle = async (): Promise<string> => {
const result = await signInWithPopup(auth, googleProvider);
// Get the ID token to send to your backend
const idToken = await result.user.getIdToken();
return idToken;
};

export { auth, googleProvider };

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check the file structure and locate related files
fd -type f -name "*.ts" -o -name "*.tsx" -o -name "*.env*" | head -20

Repository: Devasy23/splitwiser

Length of output: 233


🏁 Script executed:

#!/bin/bash
# Find TypeScript files and configuration files
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name ".env*" -o -name "vite.config.*" \) | head -30

Repository: Devasy23/splitwiser

Length of output: 697


🏁 Script executed:

#!/bin/bash
# Read the firebase.ts file to verify current state
cat -n web/services/firebase.ts

Repository: Devasy23/splitwiser

Length of output: 1225


🏁 Script executed:

#!/bin/bash
# Check AuthContext.tsx for signInWithGoogle usage and error handling
cat -n web/contexts/AuthContext.tsx

Repository: Devasy23/splitwiser

Length of output: 2548


🏁 Script executed:

#!/bin/bash
# Search for signInWithGoogle usage in the codebase
rg "signInWithGoogle" -n -A 5 -B 2

Repository: Devasy23/splitwiser

Length of output: 1429


🏁 Script executed:

#!/bin/bash
# Read Auth.tsx to see the full error handling context
cat -n web/pages/Auth.tsx | head -70

Repository: Devasy23/splitwiser

Length of output: 2765


🏁 Script executed:

#!/bin/bash
# Check vite.config.ts for environment variable configuration
cat -n web/vite.config.ts

Repository: Devasy23/splitwiser

Length of output: 803


🏁 Script executed:

#!/bin/bash
# Check for .env files in web directory and search for env variable usage patterns
echo "=== Looking for .env files in web ===" && \
find web -name ".env*" 2>/dev/null && \
echo "" && \
echo "=== Searching for VITE_ environment variable usage ===" && \
rg "VITE_|import\.meta\.env" web/ -n | head -20

Repository: Devasy23/splitwiser

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Do a thorough search for all references to signInWithGoogle to ensure we didn't miss any
rg "signInWithGoogle" -c --type ts --type tsx

Repository: Devasy23/splitwiser

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Search for all references to signInWithGoogle
rg "signInWithGoogle" -c

Repository: Devasy23/splitwiser

Length of output: 110


🌐 Web query:

Firebase web API key exposure and GCP restrictions best practices

💡 Result:

Short answer: Firebase Web API keys are not secret credentials (they identify the Firebase project, not authorize access), but an exposed key still creates risk if abused — so treat them carefully: restrict and monitor keys, enforce Firebase security (Rules + App Check), move privileged work to trusted servers, and rotate + revoke keys if needed. Sources below.

Recommended checklist (actionable):

  1. Understand risk
  • Firebase API keys identify the project but do not replace Firebase Security Rules or IAM; don’t rely on the key for authorization. [1]
  1. Apply API key restrictions in GCP
  • Add application restrictions (HTTP referrers for web, or IPs for server keys) and API restrictions (limit which Google APIs the key can call). Create separate keys per client type. [2][3]
  1. Use Firebase App Check for web
  • Enable App Check (reCAPTCHA Enterprise or reCAPTCHA v3 for web) and flip enforcement once rollout is validated — blocks unverified clients. [4]
  1. Harden backend and privileged operations
  • Keep any sensitive operations (admin actions, billing, service-account-bound calls) on server-side using service accounts / OAuth, not client API keys. Bind server keys to IPs and/or service accounts. [3]
  1. Enforce Firebase security controls
  • Implement and test Firebase Security Rules (Firestore, Realtime DB, Storage, Functions) and enable App Check enforcement to reject unauthenticated/unattested requests. [1][4]
  1. Monitoring, quotas, rotation, incident steps
  • Enable usage/quotas, billing alerts and logging (APIs & Services) and set quotas/limits on keys. If a key is abused, restrict or delete it and issue a rotated key. Audit with Cloud Audit Logs and Firebase usage metrics. [2][3]
  1. Optional: API gateway / Cloud Endpoints
  • For custom APIs, put an API gateway (Cloud Endpoints/ESPv2, Cloud API Gateway) in front and require API key verification and service-level checks (use fail-closed policies). [5]

Representative docs

  • Firebase: API keys info and guidance. [1]
  • GCP: Manage API keys & application/API restrictions. [2][3]
  • Firebase App Check + enforcement (web). [4]
  • Google Cloud Endpoints API-key restrictions / best practices. [5]

If you want, I can:

  • give exact console steps to add HTTP referrer restrictions for a key, or
  • scan your project (you share screenshots/outputs) and propose specific restrictions.

Secure Firebase API key with GCP restrictions; error handling is already in place

The Firebase initialization is sound, but the hard-coded API key in source control needs mitigation:

  1. API key exposure
    For Firebase web apps, the config (including apiKey) is intentionally public, but since this repo is in Git, ensure:

    • The key is restricted in GCP/Firebase to allowed domains (HTTP referrers) and specific Google APIs.
    • Enable Firebase App Check (reCAPTCHA Enterprise/v3) to block unverified clients.
    • Rotate and revoke the key if it was ever used with broader permissions.
    • Optional: Move config to environment variables (Vite supports this via import.meta.env) for easier key rotation per environment without code changes.
  2. Error handling note
    signInWithGoogle currently rejects on any error (popup cancel, network, etc.), but this is fine—Auth.tsx already wraps it in a comprehensive try/catch block (lines 34–54) that handles Firebase errors, API errors, and user cancellations gracefully. No changes needed here.

🧰 Tools
🪛 Gitleaks (8.29.1)

[high] 6-6: Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches.

(gcp-api-key)

🤖 Prompt for AI Agents
In web/services/firebase.ts around lines 4 to 29 the Firebase config (including
apiKey) is hard-coded in source; although web apiKeys are public by design, you
must restrict and protect its usage: restrict the key in GCP/Firebase to allowed
HTTP referrers and specific Google APIs, enable Firebase App Check (reCAPTCHA
Enterprise/v3) to block unverified clients, rotate/revoke the key if it had
broader permissions, and optionally move the config into environment variables
(e.g. Vite’s import.meta.env) so different environments can use different keys
and rotation doesn’t require code changes.

Copy link
Contributor

@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 (2)
web/pages/Groups.tsx (1)

48-50: Avoid any type in getGroupBalance.

The function parameter uses any which reduces type safety.

   const getGroupBalance = (groupId: string): GroupBalanceSummary | undefined => {
-    return balanceSummary?.groupsSummary?.find((g: any) => g.groupId === groupId);
+    return balanceSummary?.groupsSummary?.find((g) => g.groupId === groupId);
   };

If GroupBalanceSummary is properly typed in BalanceSummary.groupsSummary, TypeScript should infer the type automatically.

web/pages/Dashboard.tsx (1)

31-34: Hardcoded $ currency symbol in chart data.

The chart data hardcodes values without currency context. While the tooltip displays raw numbers, the overall dashboard lacks currency awareness that other pages now have.

♻️ Duplicate comments (11)
web/pages/Friends.tsx (2)

262-263: LGTM! Balance sign display fixed.

The balance display now correctly prepends the sign: + for positive, - for negative, empty for zero. Combined with formatCurrency using Math.abs(), this produces correct output like +$10.00 or -$10.00.


43-45: Unused gMap variable for group names.

The gMap is built from groups but groupName is already provided directly in friend.breakdown. The map is only used to enrich imageUrl. Consider simplifying:

-        const gMap = new Map<string, { name: string; imageUrl?: string }>(
-          groups.map((g: { _id: string; name: string; imageUrl?: string }) => [g._id, { name: g.name, imageUrl: g.imageUrl }])
+        const groupImageMap = new Map<string, string | undefined>(
+          groups.map((g: { _id: string; imageUrl?: string }) => [g._id, g.imageUrl])
         );
         ...
-            imageUrl: gMap.get(group.groupId)?.imageUrl
+            imageUrl: groupImageMap.get(group.groupId)
web/pages/Groups.tsx (3)

76-79: LGTM! Filtering now memoized.

The filteredGroups computation is properly wrapped in useMemo with correct dependencies [groups, searchTerm], preventing unnecessary recalculations on every render.


186-187: LGTM! Currency display fixed.

The balance display now correctly uses group.currency instead of a hardcoded $ symbol.


105-105: External URL dependency for visual asset.

The noise texture is loaded from an external service. If the service becomes unavailable, the visual effect breaks silently. Consider hosting locally or using a data URI.

web/pages/Dashboard.tsx (2)

44-46: LGTM! Nullish coalescing prevents runtime errors.

The balance values now use (summary?.field ?? 0).toFixed(2) pattern, preventing runtime errors when summary is null.

Also applies to: 54-56, 64-66


64-65: Consider extracting complex ternary for readability.

The nested ternary for Net Balance color is difficult to parse. Extract to a helper variable for clarity.

+  const netBalanceColor = summary && summary.netBalance >= 0
+    ? (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500')
+    : (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-red-500');

   return (
     ...
-          <h2 className={`text-3xl font-extrabold ${summary && summary.netBalance >= 0 ? (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500') : (style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-red-500')}`}>
+          <h2 className={`text-3xl font-extrabold ${netBalanceColor}`}>
web/pages/GroupDetails.tsx (4)

74-76: Move fetchData inside useEffect or wrap with useCallback.

The fetchData function is defined outside the effect but references id. ESLint's exhaustive-deps rule would flag this. Move it inside the effect for correctness.

     useEffect(() => {
+        const fetchData = async () => {
+            if (!id) return;
+            setLoading(true);
+            try {
+                const [groupRes, expRes, memRes, setRes] = await Promise.all([
+                    getGroupDetails(id),
+                    getExpenses(id),
+                    getGroupMembers(id),
+                    getOptimizedSettlements(id)
+                ]);
+                setGroup(groupRes.data);
+                setExpenses(expRes.data.expenses);
+                setMembers(memRes.data);
+                setSettlements(setRes.data.optimizedSettlements);
+                setEditGroupName(groupRes.data.name);
+            } catch (err) {
+                console.error(err);
+            } finally {
+                setLoading(false);
+            }
+        };
         if (id) fetchData();
     }, [id]);

314-320: Client-side unsettled balance check uses potentially stale data.

This check uses the local settlements state which may be stale if another user just settled. Let the server handle validation and display the server error instead.

         if (window.confirm(`Are you sure you want to remove ${memberName} from the group?`)) {
             try {
-                const hasUnsettled = settlements.some(
-                    s => (s.fromUserId === memberId || s.toUserId === memberId) && (s.amount || 0) > 0
-                );
-                if (hasUnsettled) {
-                    alert('Cannot remove: This member has unsettled balances in the group.');
-                    return;
-                }
                 await removeMember(id, memberId);
                 fetchData();
             } catch (err: any) {
                 alert(err.response?.data?.detail || "Failed to remove member");
             }
         }

651-668: Add keyboard accessibility to member selection checkboxes.

The div elements with onClick for member selection are not keyboard accessible. Users cannot tab to and activate these elements.

                                             <div
                                                 key={m.userId}
+                                                role="checkbox"
+                                                aria-checked={isSelected}
+                                                tabIndex={0}
                                                 onClick={() => {
                                                     const newSet = new Set(selectedUsers);
                                                     if (isSelected) newSet.delete(m.userId);
                                                     else newSet.add(m.userId);
                                                     setSelectedUsers(newSet);
                                                 }}
+                                                onKeyDown={(e) => {
+                                                    if (e.key === 'Enter' || e.key === ' ') {
+                                                        e.preventDefault();
+                                                        const newSet = new Set(selectedUsers);
+                                                        if (isSelected) newSet.delete(m.userId);
+                                                        else newSet.add(m.userId);
+                                                        setSelectedUsers(newSet);
+                                                    }
+                                                }}
                                                 className={`cursor-pointer ...`}
                                             >

742-764: Associate labels with form controls using htmlFor.

The "Payer" and "Payee" labels (lines 743, 756) are not programmatically associated with their select elements, making them less accessible for screen readers.

                     <div className="flex flex-col gap-1">
-                        <label className="text-sm font-bold opacity-80">Payer</label>
+                        <label htmlFor="payment-payer" className="text-sm font-bold opacity-80">Payer</label>
                         <select
+                            id="payment-payer"
                             className={`...`}
                             value={paymentPayerId}
                             onChange={e => setPaymentPayerId(e.target.value)}
                         >

Apply the same pattern to "Payee" at line 756.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3d90ec and 964cb22.

📒 Files selected for processing (6)
  • web/pages/Auth.tsx (2 hunks)
  • web/pages/Dashboard.tsx (4 hunks)
  • web/pages/Friends.tsx (1 hunks)
  • web/pages/GroupDetails.tsx (1 hunks)
  • web/pages/Groups.tsx (6 hunks)
  • web/pages/Profile.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-26T09:41:01.332Z
Learnt from: CR
Repo: Devasy23/splitwiser PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-26T09:41:01.332Z
Learning: Applies to backend/app/expenses/**/*.py : Settlements must track debt resolution between users in the expense tracking logic.

Applied to files:

  • web/pages/GroupDetails.tsx
🧬 Code graph analysis (4)
web/pages/Groups.tsx (6)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/constants.ts (1)
  • THEMES (1-4)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Skeleton.tsx (1)
  • Skeleton (9-23)
web/components/ui/Modal.tsx (1)
  • Modal (15-89)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/pages/Friends.tsx (3)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (2)
  • getFriendsBalance (49-49)
  • getGroups (27-27)
web/constants.ts (1)
  • THEMES (1-4)
web/pages/Auth.tsx (7)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/firebase.ts (1)
  • signInWithGoogle (21-26)
web/services/api.ts (2)
  • loginWithGoogle (23-23)
  • login (21-21)
web/constants.ts (1)
  • THEMES (1-4)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/pages/Profile.tsx (8)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (1)
  • updateProfile (50-50)
web/constants.ts (1)
  • THEMES (1-4)
web/types.ts (1)
  • User (1-8)
web/components/ui/Modal.tsx (1)
  • Modal (15-89)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Input.tsx (1)
  • Input (10-28)
🪛 Biome (2.1.2)
web/pages/GroupDetails.tsx

[error] 581-581: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 590-590: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 651-663: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 651-663: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 743-743: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 756-756: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

web/pages/Profile.tsx

[error] 135-135: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)


[error] 259-259: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)

⏰ 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: preview
🔇 Additional comments (14)
web/pages/Profile.tsx (3)

130-136: LGTM! Keyboard accessibility properly implemented.

The clickable avatar now correctly includes role="button", tabIndex={0}, keyboard event handlers, and aria-label. This addresses the accessibility requirements for interactive elements.


254-260: LGTM! Image picker accessibility properly implemented.

The image picker div correctly implements keyboard accessibility with role="button", tabIndex={0}, onKeyDown handler, and aria-label.


37-65: LGTM! Profile save logic properly refactored.

The implementation correctly:

  • Uses inline error state (saveError) instead of blocking alert() for validation
  • Uses updateUserInContext to update user state in-place instead of window.location.reload()
  • Handles errors gracefully with proper loading state management
web/pages/Friends.tsx (2)

8-22: LGTM! Proper TypeScript interfaces defined.

The GroupBreakdown and Friend interfaces provide clear type definitions for the data structures, improving type safety and IDE support.


43-67: LGTM! Inline interface replaces any types.

The FriendBalanceData interface properly types the API response, and the mapping now uses explicit types instead of any.

web/pages/GroupDetails.tsx (1)

890-898: LGTM! ScaleIcon now has accessible title.

The SVG now includes role="img", aria-labelledby, and a <title> element for screen reader accessibility.

web/pages/Auth.tsx (8)

1-15: LGTM!

Import organization is clean. Good practice aliasing login/signup as apiLogin/apiSignup to avoid naming conflicts with the login function from useAuth.


17-28: LGTM!

Separate loading states for email/password and Google sign-in prevent UI conflicts when one flow is in progress.


85-86: LGTM!

Clean derivation of theme flag for conditional styling throughout the component.


136-141: LGTM!

Good UX allowing theme switching from the authentication page. Correctly uses the reusable Button component.


166-184: SVG accessibility properly implemented.

The Google logo SVG now includes role="img", aria-labelledby, and a <title> element, providing proper accessibility for screen readers.


196-212: LGTM!

The AnimatePresence with mode="wait" correctly handles the conditional name field animation during signup. The required attribute is appropriately applied only when the field is rendered.


231-239: LGTM!

Animated error display provides clear feedback with theme-aware styling.


246-257: LGTM!

The toggle between login and signup preserves form field values, which is good UX as users don't lose their input when switching modes.

{/* Left Side - Visuals */}
<div className="hidden lg:flex lg:w-1/2 relative overflow-hidden bg-black text-white items-center justify-center p-12">
<div className="absolute inset-0 bg-gradient-to-br from-blue-600 to-purple-900 opacity-50" />
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20" />
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

External asset dependency may affect reliability.

The noise texture references an external URL (grainy-gradients.vercel.app). If this service becomes unavailable, the visual effect will silently fail (acceptable degradation), but consider hosting the asset locally for reliability.

🤖 Prompt for AI Agents
In web/pages/Auth.tsx around line 92 the background image references an external
noise SVG which can be unreliable; download that SVG into the repo (e.g.
public/images/noise.svg), add the file to source control, and change the
background URL to point to the local path (e.g. url('/images/noise.svg')) so the
app serves it from our static assets; ensure the file is included in the build
and consider keeping the external URL only as an optional fallback if desired.

Comment on lines 163 to 165
{googleLoading ? (
<div className="w-5 h-5 border-2 border-black/20 border-t-black rounded-full animate-spin" />
) : (
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add accessibility attributes to the loading spinner.

The spinner lacks accessibility information. Screen reader users won't know the button is in a loading state.

               {googleLoading ? (
-                <div className="w-5 h-5 border-2 border-black/20 border-t-black rounded-full animate-spin" />
+                <div 
+                  className="w-5 h-5 border-2 border-black/20 border-t-black rounded-full animate-spin"
+                  role="status"
+                  aria-label="Signing in with Google"
+                />
               ) : (
📝 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
{googleLoading ? (
<div className="w-5 h-5 border-2 border-black/20 border-t-black rounded-full animate-spin" />
) : (
{googleLoading ? (
<div
className="w-5 h-5 border-2 border-black/20 border-t-black rounded-full animate-spin"
role="status"
aria-label="Signing in with Google"
/>
) : (
🤖 Prompt for AI Agents
In web/pages/Auth.tsx around lines 163 to 165, the loading spinner lacks
accessibility attributes; add role="status" and aria-live="polite" to the
spinner element and include an offscreen/plain-text label (e.g., a span with the
project's "sr-only" class containing "Loading…") so screen readers announce the
state, and also set aria-busy="true" on the parent button when googleLoading is
true to indicate the control is busy.

Comment on lines +74 to +76
const handleComingSoon = () => {
alert('This feature is coming soon!');
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Replace alert() with toast notification for consistency.

The "coming soon" handler still uses alert(), which is inconsistent with the improved UX pattern used elsewhere in the component (e.g., saveError state for validation). Consider using a toast notification system.

🤖 Prompt for AI Agents
In web/pages/Profile.tsx around lines 74-76, replace the blocking browser alert
used in handleComingSoon() with the app's toast notification pattern: import and
use the existing toast utility/component used elsewhere in the app (or reuse the
component/state used for saveError) to display "This feature is coming soon!"
with a non-blocking info/neutral variant, remove the alert() call, and ensure
any required import is added and types updated.

Copy link
Contributor

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

♻️ Duplicate comments (6)
web/pages/GroupDetails.tsx (4)

82-84: Missing fetchData in useEffect dependencies.

The effect calls fetchData which is defined outside but references id. ESLint's exhaustive-deps rule would flag this.

Either move fetchData inside the effect, wrap it with useCallback, or suppress with an ESLint comment:

     useEffect(() => {
         if (id) fetchData();
+        // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [id]);

316-335: Client-side unsettled balance check uses potentially stale data.

The settlements.some(...) check uses local state that may be stale if another user just settled. This could incorrectly block removal. The server should be the source of truth for validation.

Consider removing the client-side pre-check and relying on server validation:

         if (window.confirm(`Are you sure you want to remove ${memberName} from the group?`)) {
             try {
-                const hasUnsettled = settlements.some(
-                    s => (s.fromUserId === memberId || s.toUserId === memberId) && (s.amount || 0) > 0
-                );
-                if (hasUnsettled) {
-                    alert('Cannot remove: This member has unsettled balances in the group.');
-                    return;
-                }
                 await removeMember(id, memberId);
                 fetchData();
             } catch (err: any) {
                 alert(err.response?.data?.detail || "Failed to remove member");
             }

659-677: Add keyboard accessibility to member selection checkboxes.

The div elements with onClick handlers for member selection are not keyboard accessible. Users cannot tab to and activate these elements.

                                             <div
                                                 key={m.userId}
+                                                role="checkbox"
+                                                aria-checked={isSelected}
+                                                tabIndex={0}
                                                 onClick={() => {
                                                     const newSet = new Set(selectedUsers);
                                                     if (isSelected) newSet.delete(m.userId);
                                                     else newSet.add(m.userId);
                                                     setSelectedUsers(newSet);
                                                 }}
+                                                onKeyDown={(e) => {
+                                                    if (e.key === 'Enter' || e.key === ' ') {
+                                                        e.preventDefault();
+                                                        const newSet = new Set(selectedUsers);
+                                                        if (isSelected) newSet.delete(m.userId);
+                                                        else newSet.add(m.userId);
+                                                        setSelectedUsers(newSet);
+                                                    }
+                                                }}
                                                 className={`cursor-pointer flex items-center gap-2 p-2 border ...`}
                                             >

750-772: Associate labels with their form controls.

Labels for "Payer" and "Payee" selects are not programmatically associated with their inputs, making them inaccessible to screen readers.

                     <div className="flex flex-col gap-1">
-                        <label className="text-sm font-bold opacity-80">Payer</label>
+                        <label htmlFor="payment-payer" className="text-sm font-bold opacity-80">Payer</label>
                         <select
+                            id="payment-payer"
                             className={`w-full p-3 outline-none font-bold ...`}
                             value={paymentPayerId}
                             onChange={e => setPaymentPayerId(e.target.value)}
                         >

Apply similarly to "Payee" at line 764.

web/pages/Profile.tsx (1)

85-87: Consider replacing alert() with toast notification.

The handleComingSoon function uses alert() which is inconsistent with the polished UI design. A toast notification would provide better UX.

web/pages/Auth.tsx (1)

101-101: External asset dependency noted.

The noise texture references an external URL (grainy-gradients.vercel.app). While this will degrade gracefully if unavailable, consider hosting locally for reliability in production.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 964cb22 and f867469.

📒 Files selected for processing (4)
  • web/pages/Auth.tsx (2 hunks)
  • web/pages/Friends.tsx (1 hunks)
  • web/pages/GroupDetails.tsx (1 hunks)
  • web/pages/Profile.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-26T09:41:01.332Z
Learnt from: CR
Repo: Devasy23/splitwiser PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-26T09:41:01.332Z
Learning: Applies to backend/app/expenses/**/*.py : Settlements must track debt resolution between users in the expense tracking logic.

Applied to files:

  • web/pages/GroupDetails.tsx
🧬 Code graph analysis (3)
web/pages/Auth.tsx (7)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/firebase.ts (1)
  • signInWithGoogle (21-26)
web/services/api.ts (2)
  • loginWithGoogle (23-23)
  • login (21-21)
web/constants.ts (1)
  • THEMES (1-4)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/pages/GroupDetails.tsx (8)
web/types.ts (4)
  • Settlement (58-68)
  • Group (16-25)
  • Expense (46-56)
  • GroupMember (27-32)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (12)
  • getGroupDetails (29-29)
  • getExpenses (36-36)
  • getGroupMembers (32-32)
  • getOptimizedSettlements (44-44)
  • updateExpense (38-38)
  • createExpense (37-37)
  • deleteExpense (39-39)
  • createSettlement (43-43)
  • updateGroup (30-30)
  • deleteGroup (31-31)
  • leaveGroup (53-53)
  • removeMember (54-54)
web/constants.ts (1)
  • THEMES (1-4)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Modal.tsx (1)
  • Modal (15-89)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/pages/Profile.tsx (8)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (1)
  • updateProfile (50-50)
web/constants.ts (1)
  • THEMES (1-4)
web/types.ts (1)
  • User (1-8)
web/components/ui/Modal.tsx (1)
  • Modal (15-89)
web/components/ui/Button.tsx (1)
  • Button (10-55)
web/components/ui/Input.tsx (1)
  • Input (10-28)
🪛 Biome (2.1.2)
web/pages/GroupDetails.tsx

[error] 589-589: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 598-598: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 659-671: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 659-671: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 751-751: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 764-764: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

web/pages/Profile.tsx

[error] 146-146: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)


[error] 270-270: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)

⏰ 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: preview
🔇 Additional comments (13)
web/pages/GroupDetails.tsx (2)

898-907: LGTM! Accessible SVG implementation.

The ScaleIcon correctly includes role="img", aria-labelledby, and a <title> element for screen reader accessibility.


103-123: LGTM! Clean parallel data fetching.

The fetchData function correctly uses Promise.all to fetch group details, expenses, members, and settlements in parallel, with proper error handling and loading state management.

web/pages/Friends.tsx (4)

8-22: LGTM! Well-defined TypeScript interfaces.

The GroupBreakdown and Friend interfaces provide proper type safety for the data structures used throughout the component.


43-67: LGTM! Properly typed data transformation.

The data transformation now uses typed interfaces (FriendBalanceData) instead of any, addressing the previous type safety concerns.


236-243: Net balance display now includes sign indicator.

The balance rendering correctly prefixes + for positive and - for negative balances, consistent with the group breakdown display.


270-275: LGTM! Button has explicit type attribute.

The "View Details" button correctly includes type="button" to prevent unintended form submissions.

web/pages/Profile.tsx (3)

25-46: LGTM! Image validation implemented.

The handleImagePick function properly validates file size (5MB limit) and file type before processing, preventing oversized base64 payloads.


65-69: LGTM! Context update instead of page reload.

The profile update flow correctly uses updateUserInContext to update local state instead of forcing a full page reload, preserving client-side state and improving UX.


203-219: LGTM! Menu buttons have explicit type attribute.

The menu section buttons correctly include type="button" to prevent unintended form submissions.

web/pages/Auth.tsx (4)

30-60: LGTM! Robust Google sign-in implementation.

The handleGoogleSignIn function includes:

  • Defensive checks for access_token and user in the response
  • Proper handling of popup-closed-by-user scenario
  • Error handling for both string and array detail formats from the backend

78-88: LGTM! Consistent error handling in email/password flow.

The handleSubmit error handling now properly handles both string and array formats for the backend detail field, matching the pattern used in Google sign-in.


172-197: LGTM! Accessible loading spinner and Google logo.

The loading spinner includes role="status" and aria-label for screen readers. The Google logo SVG properly includes a <title> element with aria-labelledby.


208-257: LGTM! Well-structured form with animations.

The email/password form correctly uses AnimatePresence for the conditional name field during signup, with proper form submission handling and error display.

Comment on lines +588 to +593
<div className="w-24 flex flex-col gap-1">
<label className="text-sm font-semibold opacity-80">Currency</label>
<div className={`w-full p-3 font-bold text-center opacity-80 ${style === THEMES.NEOBRUTALISM ? 'border-2 border-black bg-white rounded-none' : 'bg-white/10 border border-white/20 rounded-lg'}`}>
{currency}
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Associate currency label with its display element.

The "Currency" label at line 589 is not associated with any control.

Since this is a read-only display, consider using aria-labelledby or wrapping in a <dl> structure:

-                            <div className="w-24 flex flex-col gap-1">
-                                <label className="text-sm font-semibold opacity-80">Currency</label>
+                            <div className="w-24 flex flex-col gap-1" role="group" aria-label="Currency">
+                                <span className="text-sm font-semibold opacity-80">Currency</span>
                                 <div className={`w-full p-3 font-bold text-center opacity-80 ...`}>
                                     {currency}
                                 </div>
                             </div>
🧰 Tools
🪛 Biome (2.1.2)

[error] 589-589: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

🤖 Prompt for AI Agents
In web/pages/GroupDetails.tsx around lines 588 to 593, the "Currency" label is
not programmatically associated with the read-only display element; update the
markup so the label is connected to the display either by wrapping the pair in a
semantic description list (<dl> with <dt> for the label and <dd> for the value)
or by giving the value element a unique id and adding aria-labelledby to it that
references the label's id, ensuring the label text remains visible and the
display remains read-only and styled as before.

Comment on lines +597 to +614
<div>
<label className="block text-sm font-bold mb-2 opacity-80">Paid By</label>
<div className="flex flex-wrap gap-2">
{members.map(m => (
<button
key={m.userId}
type="button"
onClick={() => setPayerId(m.userId)}
className={`px-4 py-2 text-sm font-bold transition-all border ${payerId === m.userId
? (style === THEMES.NEOBRUTALISM ? 'bg-black text-white border-black rounded-none' : 'bg-blue-600 border-blue-500 text-white rounded-full')
: (style === THEMES.NEOBRUTALISM ? 'bg-white text-black border-black hover:bg-gray-100 rounded-none' : 'bg-transparent border-gray-600 text-gray-400 hover:border-gray-400 rounded-full')
}`}
>
{m.userId === user?._id ? 'You' : m.user?.name}
</button>
))}
</div>
Split Unequally
</button>
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Paid By" label lacks association with the button group.

The label "Paid By" at line 598 is not programmatically linked to the selection controls.

Consider using a fieldset with legend for proper semantics:

-                    <div>
-                        <label className="block text-sm font-bold mb-2 opacity-80">Paid By</label>
+                    <fieldset>
+                        <legend className="block text-sm font-bold mb-2 opacity-80">Paid By</legend>
                         <div className="flex flex-wrap gap-2">
                             {members.map(m => (
                                 <button

Or use role="group" with aria-label.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (2.1.2)

[error] 598-598: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

🤖 Prompt for AI Agents
In web/pages/GroupDetails.tsx around lines 597 to 614, the "Paid By" label is
not programmatically associated with the button group; wrap the button list in a
semantic container (preferably a <fieldset> with a <legend> containing "Paid
By") or at minimum add a container element with role="group" and
aria-label="Paid By", and ensure each toggle button exposes its state (e.g.,
aria-pressed or aria-checked) so assistive tech can identify the group and the
selected payer.

Comment on lines +141 to +167
<div
className="relative group cursor-pointer"
onClick={openEditModal}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openEditModal(); } }}
tabIndex={0}
role="button"
aria-label="Edit profile picture"
>
<div className={`w-32 h-32 p-1 ${isNeo ? 'bg-black rounded-none' : 'bg-gradient-to-br from-blue-500 to-purple-500 rounded-full'}`}>
{isValidImageUrl ? (
<img
src={avatarUrl}
alt={user?.name}
className={`w-full h-full object-cover border-4 border-white dark:border-gray-900 ${isNeo ? 'rounded-none' : 'rounded-full'}`}
/>
) : (
<div className={`w-full h-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-4xl font-bold text-gray-500 border-4 border-white dark:border-gray-900 ${isNeo ? 'rounded-none' : 'rounded-full'}`}>
{user?.name?.charAt(0) || 'A'}
</div>
)}
</div>
<div className={`absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity ${isNeo ? 'rounded-none' : 'rounded-full'}`}>
<div className="bg-black/50 p-2 text-white rounded-full">
<Camera size={24} />
</div>
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using a semantic <button> element for the avatar.

While the keyboard accessibility attributes are correctly added (tabIndex, role="button", onKeyDown), using a native <button> element would provide these behaviors automatically and is more semantically correct.

-                        <div
+                        <button
+                            type="button"
                             className="relative group cursor-pointer"
                             onClick={openEditModal}
-                            onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openEditModal(); } }}
-                            tabIndex={0}
-                            role="button"
                             aria-label="Edit profile picture"
                         >
                             ...
-                        </div>
+                        </button>

Apply similarly to the image picker at lines 265-287.

📝 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
<div
className="relative group cursor-pointer"
onClick={openEditModal}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openEditModal(); } }}
tabIndex={0}
role="button"
aria-label="Edit profile picture"
>
<div className={`w-32 h-32 p-1 ${isNeo ? 'bg-black rounded-none' : 'bg-gradient-to-br from-blue-500 to-purple-500 rounded-full'}`}>
{isValidImageUrl ? (
<img
src={avatarUrl}
alt={user?.name}
className={`w-full h-full object-cover border-4 border-white dark:border-gray-900 ${isNeo ? 'rounded-none' : 'rounded-full'}`}
/>
) : (
<div className={`w-full h-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-4xl font-bold text-gray-500 border-4 border-white dark:border-gray-900 ${isNeo ? 'rounded-none' : 'rounded-full'}`}>
{user?.name?.charAt(0) || 'A'}
</div>
)}
</div>
<div className={`absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity ${isNeo ? 'rounded-none' : 'rounded-full'}`}>
<div className="bg-black/50 p-2 text-white rounded-full">
<Camera size={24} />
</div>
</div>
</div>
<button
type="button"
className="relative group cursor-pointer"
onClick={openEditModal}
aria-label="Edit profile picture"
>
<div className={`w-32 h-32 p-1 ${isNeo ? 'bg-black rounded-none' : 'bg-gradient-to-br from-blue-500 to-purple-500 rounded-full'}`}>
{isValidImageUrl ? (
<img
src={avatarUrl}
alt={user?.name}
className={`w-full h-full object-cover border-4 border-white dark:border-gray-900 ${isNeo ? 'rounded-none' : 'rounded-full'}`}
/>
) : (
<div className={`w-full h-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-4xl font-bold text-gray-500 border-4 border-white dark:border-gray-900 ${isNeo ? 'rounded-none' : 'rounded-full'}`}>
{user?.name?.charAt(0) || 'A'}
</div>
)}
</div>
<div className={`absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity ${isNeo ? 'rounded-none' : 'rounded-full'}`}>
<div className="bg-black/50 p-2 text-white rounded-full">
<Camera size={24} />
</div>
</div>
</button>
🧰 Tools
🪛 Biome (2.1.2)

[error] 146-146: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)

🤖 Prompt for AI Agents
In web/pages/Profile.tsx around lines 141 to 167, the avatar wrapper is using a
div with manual keyboard handlers and role="button" which is less semantic and
duplicates built-in behavior; replace the outer div with a native <button>
(preserve className, onClick, aria-label, tabIndex can be removed since button
is focusable, and move onKeyDown logic into the button or remove because
Enter/Space are natively handled) and ensure you keep the inner layout and
Camera overlay styles; also apply the same change to the image picker at lines
265 to 287 (swap the div for a button, retain styling and handlers, and remove
redundant accessibility props).

Copy link
Contributor

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

♻️ Duplicate comments (6)
web/pages/GroupDetails.tsx (6)

82-84: Missing fetchData in useEffect dependencies.

This is a duplicate of a previous comment. The effect calls fetchData which is defined outside but the dependency array only includes id. Consider wrapping fetchData in useCallback with id as a dependency, or move the function inside the effect.


316-335: Client-side unsettled balance check is unreliable.

This is a duplicate of a previous comment. The check uses potentially stale client data from settlements. If another user just settled, this check would incorrectly prevent removal. The server should be the source of truth for this validation.


663-681: Add keyboard accessibility to member selection checkboxes.

This is a duplicate of a previous comment. The div elements with onClick handlers are not keyboard accessible. Users cannot tab to and activate these elements. Add role="checkbox", aria-checked, tabIndex={0}, and an onKeyDown handler for Enter/Space keys.


592-597: Associate currency label with its display element.

This is a duplicate of a previous comment. The "Currency" label is not programmatically associated with any control. Since this is read-only, consider using aria-labelledby or changing <label> to <span>.


601-618: "Paid By" label lacks association with the button group.

This is a duplicate of a previous comment. Consider using a <fieldset> with <legend> for proper semantics, or use role="group" with aria-label.


754-763: Associate labels with their form controls.

This is a duplicate of a previous comment. Labels for "Payer" and "Payee" selects are not programmatically associated with their inputs.

                     <div className="flex flex-col gap-1">
-                        <label className="text-sm font-bold opacity-80">Payer</label>
+                        <label htmlFor="payment-payer" className="text-sm font-bold opacity-80">Payer</label>
                         <select
+                            id="payment-payer"
                             className={`w-full p-3 outline-none font-bold ...`}
                             value={paymentPayerId}
                             onChange={e => setPaymentPayerId(e.target.value)}
                         >

Apply similarly to "Payee" select at line 768-775.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f867469 and b8d4c05.

📒 Files selected for processing (2)
  • web/pages/Friends.tsx (1 hunks)
  • web/pages/GroupDetails.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-26T09:41:01.332Z
Learnt from: CR
Repo: Devasy23/splitwiser PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-26T09:41:01.332Z
Learning: Applies to backend/app/expenses/**/*.py : Settlements must track debt resolution between users in the expense tracking logic.

Applied to files:

  • web/pages/GroupDetails.tsx
🧬 Code graph analysis (2)
web/pages/GroupDetails.tsx (8)
web/types.ts (4)
  • Settlement (58-68)
  • Group (16-25)
  • Expense (46-56)
  • GroupMember (27-32)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (12)
  • getGroupDetails (29-29)
  • getExpenses (36-36)
  • getGroupMembers (32-32)
  • getOptimizedSettlements (44-44)
  • updateExpense (38-38)
  • createExpense (37-37)
  • deleteExpense (39-39)
  • createSettlement (43-43)
  • updateGroup (30-30)
  • deleteGroup (31-31)
  • leaveGroup (53-53)
  • removeMember (54-54)
web/components/ui/Skeleton.tsx (1)
  • Skeleton (9-23)
web/constants.ts (1)
  • THEMES (1-4)
web/components/ui/Modal.tsx (1)
  • Modal (15-89)
web/components/ui/Input.tsx (1)
  • Input (10-28)
web/pages/Friends.tsx (3)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (1)
  • getGroups (27-27)
web/constants.ts (1)
  • THEMES (1-4)
🪛 Biome (2.1.2)
web/pages/GroupDetails.tsx

[error] 449-449: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)


[error] 593-593: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 602-602: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 663-675: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 663-675: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 755-755: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 768-768: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

web/pages/Friends.tsx

[error] 219-219: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)

⏰ 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: preview
🔇 Additional comments (9)
web/pages/GroupDetails.tsx (5)

1-35: LGTM!

Imports are well-organized, and the local Settlement interface provides proper type safety as previously requested.


77-80: LGTM!

The isAdmin memoization correctly depends on members and user?._id, ensuring it updates when either changes.


350-406: LGTM!

The header section is well-structured with proper button types and clean animation implementation.


788-897: LGTM!

The settings modal is well-organized with proper tab structure, button types, and admin-gated dangerous actions.


902-911: LGTM!

The ScaleIcon component now has proper accessibility with role="img", aria-labelledby, and a <title> element as previously requested.

web/pages/Friends.tsx (4)

1-22: LGTM!

The imports are clean and the TypeScript interfaces GroupBreakdown and Friend are well-defined at module level, providing good type safety for the component.


79-113: LGTM!

The helper functions are well-implemented. The formatCurrency helper correctly returns absolute values with the sign handled separately in the render logic, and getAvatarContent properly validates image URLs before rendering.


156-193: LGTM!

The summary cards are well-structured with proper theme-aware styling and staggered animations for a polished UX.


275-280: I'll verify whether the "View Details" button is truly non-functional by examining the full context and searching for any onClick handlers or navigation logic.
<function_calls>


#!/bin/bash

First, let's read the Friends.tsx file around lines 275-280 with context

wc -l web/pages/Friends.tsx


</function_calls>
<function_calls>


#!/bin/bash

Read the relevant section with more context (lines 250-290)

sed -n '250,290p' web/pages/Friends.tsx | cat -n


</function_calls>
<function_calls>


#!/bin/bash

Search for onClick handlers related to "View Details" button in Friends.tsx

rg -n "View Details" web/pages/Friends.tsx -B 10 -A 5


</function_calls>
<function_calls>


#!/bin/bash

Search for any onClick handlers in the same file, especially around expanded groups

rg -n "onClick" web/pages/Friends.tsx -C 3


</function_calls>
<function_calls>


#!/bin/bash

Search for FriendDetail or friend detail routes/components in the codebase

rg -n "FriendDetail|friend.*[Dd]etail" --type=tsx --type=ts | head -20


</function_calls>

@Devasy Devasy merged commit b963fd5 into main Nov 27, 2025
38 checks passed
@Devasy Devasy deleted the feat/web-ui-sso-profile-group-settings branch November 27, 2025 18:33
Copy link
Contributor

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

♻️ Duplicate comments (8)
web/pages/Friends.tsx (2)

48-54: Move FriendBalanceData interface to module level.

The interface is still defined inside the useEffect callback. This is unconventional and reduces reusability. Move it alongside the other interfaces (GroupBreakdown, Friend) at the top of the file for consistency.

+interface FriendBalanceData {
+  userId: string;
+  userName: string;
+  userImageUrl?: string;
+  netBalance: number;
+  breakdown?: { groupId: string; groupName: string; balance: number }[];
+}
+
 interface GroupBreakdown {
   groupId: string;
   groupName: string;

Then remove lines 48-54 from inside the useEffect.


129-129: Consider bundling the noise texture locally.

The external URL grainy-gradients.vercel.app creates a runtime dependency on a third-party service. If unavailable, the texture won't load. Download and serve this asset locally for reliability.

web/pages/GroupDetails.tsx (6)

82-85: Align useEffect dependencies with fetchData definition (exhaustive-deps).

The effect at Lines 82-85 calls fetchData, which is defined outside the effect, but the dependency array only lists id. This will keep tripping exhaustive-deps and risks subtle bugs if fetchData ever starts capturing more values.

A common fix is to memoize fetchData with useCallback and include it in the effect deps:

-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';

-    const fetchData = async () => {
+    const fetchData = useCallback(async () => {
         if (!id) return;
         setLoading(true);
         try {
             const [groupRes, expRes, memRes, setRes = await Promise.all([
                 getGroupDetails(id),
                 getExpenses(id),
                 getGroupMembers(id),
                 getOptimizedSettlements(id)
             ]);
             // ...
         } catch (err) {
             console.error(err);
         } finally {
             setLoading(false);
         }
-    };
+    }, [id]);

     useEffect(() => {
-        if (id) fetchData();
-    }, [id]);
+        if (id) fetchData();
+    }, [id, fetchData]);

This should satisfy the linter and keep the data-fetch behavior stable.

Also applies to: 103-123


332-351: Client-side unsettled-balance check is still stale and redundant.

handleKickMember still inspects the local settlements array to block removal when there are unsettled balances. Because this data can be stale (another member may have just settled on another client), this check can incorrectly prevent a valid removal. The server already has the authoritative state and can enforce this invariant more reliably.

I’d recommend:

  • Removing the hasUnsettled pre-check, and
  • Letting removeMember run and surfacing any server-side validation error from the catch block.
         if (window.confirm(`Are you sure you want to remove ${memberName} from the group?`)) {
             try {
-                const hasUnsettled = settlements.some(
-                    s => (s.fromUserId === memberId || s.toUserId === memberId) && (s.amount || 0) > 0
-                );
-                if (hasUnsettled) {
-                    alert('Cannot remove: This member has unsettled balances in the group.');
-                    return;
-                }
                 await removeMember(id, memberId);
                 fetchData();
             } catch (err: any) {
                 alert(err.response?.data?.detail || "Failed to remove member");
             }
         }

608-613: Fix “Currency” label a11y by not using <label> without a control.

The “Currency” text is rendered as a <label> but not associated with any input, which Biome flags. Since this is a read-only display, it’s better treated as a labeled group or description, not a form label.

For example:

-                            <div className="w-24 flex flex-col gap-1">
-                                <label className="text-sm font-semibold opacity-80">Currency</label>
+                            <div className="w-24 flex flex-col gap-1" role="group" aria-label="Currency">
+                                <span className="text-sm font-semibold opacity-80">Currency</span>
                                 <div className={`w-full p-3 font-bold text-center opacity-80 ${style === THEMES.NEOBRUTALISM ? 'border-2 border-black bg-white rounded-none' : 'bg-white/10 border border-white/20 rounded-lg'}`}>
                                     {currency}
                                 </div>
                             </div>

This removes the invalid label usage while keeping the same visual structure.


617-634: Associate “Paid By” text with its controls using fieldset/legend.

The “Paid By” label is not programmatically tied to the button group, which Biome flags as a label-without-control issue. A <fieldset> with a <legend> gives better semantics for grouped selection controls.

-                    <div>
-                        <label className="block text-sm font-bold mb-2 opacity-80">Paid By</label>
+                    <fieldset>
+                        <legend className="block text-sm font-bold mb-2 opacity-80">Paid By</legend>
                         <div className="flex flex-wrap gap-2">
                             {members.map(m => (
                                 <button
                                     key={m.userId}
                                     type="button"
                                     onClick={() => setPayerId(m.userId)}
                                     className={`px-4 py-2 text-sm font-bold transition-all border ${payerId === m.userId
                                         ? (style === THEMES.NEOBRUTALISM ? 'bg-black text-white border-black rounded-none' : 'bg-blue-600 border-blue-500 text-white rounded-full')
                                         : (style === THEMES.NEOBRUTALISM ? 'bg-white text-black border-black hover:bg-gray-100 rounded-none' : 'bg-transparent border-gray-600 text-gray-400 hover:border-gray-400 rounded-full')
                                         }`}
                                 >
                                     {m.userId === user?._id ? 'You' : m.user?.name}
                                 </button>
                             ))}
-                        </div>
-                    </div>
+                        </div>
+                    </fieldset>

675-699: Make member selection tiles keyboard-accessible and non-static.

The member selection tiles in the “Split Equally” section are clickable <div>s without keyboard handlers or roles, which Biome flags (noStaticElementInteractions, useKeyWithClickEvents). This makes them inaccessible to keyboard users.

You can keep them as divs but add checkbox semantics and keyboard support:

-                                            <div
-                                                key={m.userId}
-                                                onClick={() => {
-                                                    const newSet = new Set(selectedUsers);
-                                                    if (isSelected) newSet.delete(m.userId);
-                                                    else newSet.add(m.userId);
-                                                    setSelectedUsers(newSet);
-                                                }}
+                                            <div
+                                                key={m.userId}
+                                                role="checkbox"
+                                                aria-checked={isSelected}
+                                                tabIndex={0}
+                                                onClick={() => {
+                                                    const newSet = new Set(selectedUsers);
+                                                    if (isSelected) newSet.delete(m.userId);
+                                                    else newSet.add(m.userId);
+                                                    setSelectedUsers(newSet);
+                                                }}
+                                                onKeyDown={(e) => {
+                                                    if (e.key === 'Enter' || e.key === ' ') {
+                                                        e.preventDefault();
+                                                        const newSet = new Set(selectedUsers);
+                                                        if (isSelected) newSet.delete(m.userId);
+                                                        else newSet.add(m.userId);
+                                                        setSelectedUsers(newSet);
+                                                    }
+                                                }}
                                                 className={`cursor-pointer flex items-center gap-2 p-2 border transition-all ${isSelected

This should resolve both Biome errors and improve a11y.


770-791: Associate “Payer”/“Payee” labels with their <select> controls.

In the payment modal, the “Payer” and “Payee” labels are not linked to their select elements, which Biome flags (noLabelWithoutControl). Add htmlFor on labels and matching id on the selects:

                     <div className="flex flex-col gap-1">
-                        <label className="text-sm font-bold opacity-80">Payer</label>
-                        <select
+                        <label htmlFor="payment-payer" className="text-sm font-bold opacity-80">Payer</label>
+                        <select
+                            id="payment-payer"
                             className={`w-full p-3 outline-none font-bold ${style === THEMES.NEOBRUTALISM ? 'border-2 border-black bg-white rounded-none' : 'bg-white/10 border border-white/20 rounded-lg text-white'}`}
                             value={paymentPayerId}
                             onChange={e => setPaymentPayerId(e.target.value)}
                         >
@@
                     <div className="flex flex-col gap-1">
-                        <label className="text-sm font-bold opacity-80">Payee</label>
-                        <select
+                        <label htmlFor="payment-payee" className="text-sm font-bold opacity-80">Payee</label>
+                        <select
+                            id="payment-payee"
                             className={`w-full p-3 outline-none font-bold ${style === THEMES.NEOBRUTALISM ? 'border-2 border-black bg-white rounded-none' : 'bg-white/10 border border-white/20 rounded-lg text-white'}`}
                             value={paymentPayeeId}
                             onChange={e => setPaymentPayeeId(e.target.value)}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8d4c05 and 6bbb1b2.

📒 Files selected for processing (2)
  • web/pages/Friends.tsx (1 hunks)
  • web/pages/GroupDetails.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-26T09:41:01.332Z
Learnt from: CR
Repo: Devasy23/splitwiser PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-26T09:41:01.332Z
Learning: Applies to backend/app/expenses/**/*.py : Settlements must track debt resolution between users in the expense tracking logic.

Applied to files:

  • web/pages/GroupDetails.tsx
🧬 Code graph analysis (2)
web/pages/GroupDetails.tsx (5)
web/types.ts (4)
  • Settlement (58-68)
  • Group (16-25)
  • Expense (46-56)
  • GroupMember (27-32)
web/contexts/AuthContext.tsx (1)
  • useAuth (62-68)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (12)
  • getGroupDetails (29-29)
  • getExpenses (36-36)
  • getGroupMembers (32-32)
  • getOptimizedSettlements (44-44)
  • updateExpense (38-38)
  • createExpense (37-37)
  • deleteExpense (39-39)
  • createSettlement (43-43)
  • updateGroup (30-30)
  • deleteGroup (31-31)
  • leaveGroup (53-53)
  • removeMember (54-54)
web/constants.ts (1)
  • THEMES (1-4)
web/pages/Friends.tsx (4)
web/contexts/ThemeContext.tsx (1)
  • useTheme (43-49)
web/services/api.ts (2)
  • getFriendsBalance (49-49)
  • getGroups (27-27)
mobile/utils/currency.js (2)
  • formatCurrency (8-16)
  • formatCurrency (8-16)
web/constants.ts (1)
  • THEMES (1-4)
🪛 Biome (2.1.2)
web/pages/GroupDetails.tsx

[error] 465-465: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)


[error] 609-609: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 618-618: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 679-691: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)


[error] 679-691: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 771-771: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)


[error] 784-784: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

⏰ 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: preview
🔇 Additional comments (6)
web/pages/Friends.tsx (4)

1-22: LGTM!

The imports are well-organized and the module-level interfaces (GroupBreakdown, Friend) provide clear type safety for the component's data structures.


82-116: LGTM!

Helper functions are well-implemented. The formatCurrency returning absolute values with signs handled at call sites is consistent throughout. The getAvatarContent provides proper fallback handling with theme-aware styling.


159-196: LGTM!

Summary cards are well-implemented with proper animation staggering and theme-aware styling. The visual hierarchy clearly communicates the balance summary.


248-276: LGTM!

The friend card header properly uses a semantic <button> element with appropriate accessibility attributes (type="button", aria-expanded, aria-label). The balance display now correctly shows signs for positive and negative values.

web/pages/GroupDetails.tsx (2)

515-520: Nice improvement using a stable composite key for settlements.

Switching from the array index to a composite key (${s.fromUserId}-${s.toUserId}) for the settlements grid avoids React key issues when the list changes order or items are added/removed.


201-252: I'll help you verify this review comment about the expense amount validation in GroupDetails.tsx. Let me start by examining the repository structure and the specific file mentioned.
<function_calls>

find . -type f -name "GroupDetails.tsx" 2>/dev/null | head -5

</function_calls>
<function_calls>

fd -t f "GroupDetails" 2>/dev/null

</function_calls>
<function_calls>

git ls-files | grep -i "groupdetails"

</function_calls>

Now let me search for the file in the web directory:
<function_calls>

find web -name "GroupDetails.tsx" 2>/dev/null

</function_calls>

Let me check what's in the web directory:
<function_calls>

ls -la web/ 2>/dev/null | head -20

</function_calls>

Let me try a broader search:
<function_calls>

git ls-files | head -30

</function_calls>

Let me search for any TypeScript/React files:
<function_calls>

fd -t f ".(tsx|ts|jsx)$" 2>/dev/null | grep -i group

</function_calls>

Let me check the repository structure:
<function_calls>

ls -la

</function_calls>

Now let me search for the handleExpenseSubmit function to locate the file:
<function_calls>

rg -l "handleExpenseSubmit" 2>/dev/null

</function_calls>

Let me check all TypeScript/TSX files:
<function_calls>

fd -e tsx -e ts 2>/dev/null | head -20

</function_calls>

Let me try a different approach and search more broadly:
<function_calls>

rg "handleExpenseSubmit" -t ts -t tsx 2>/dev/null

</function_calls>

Let me check if there are any files in the repo:
<function_calls>

git ls-files | wc -l

</function_calls>

Let me try to understand the repo better:
<function_calls>

git log --oneline -1

</function_calls>

Let me check the current branch:
<function_calls>

git branch -a

</function_calls>

Let me list all files in the repository:
<function_calls>

git ls-files

</function_calls>

Now let me read the GroupDetails.tsx file to examine the handleExpenseSubmit function:
<function_calls>

wc -l web/pages/GroupDetails.tsx

</function_calls>

Let me read the relevant section around lines 201-252:
<function_calls>

sed -n '180,260p' web/pages/GroupDetails.tsx

</function_calls>

Let me also check what comes before to understand the full context of the component:
<function_calls>

sed -n '1,80p' web/pages/GroupDetails.tsx

</function_calls>

Let me search for the expense input field and form setup:
<function_calls>

rg -A 5 -B 5 "amount.*input|setAmount" web/pages/GroupDetails.tsx

</function_calls>

Let me check for the form JSX that renders the input:
<function_calls>

sed -n '300,400p' web/pages/GroupDetails.tsx

</function_calls>

Let me search for the Input component and its configuration:
<function_calls>

rg -n "Input.*amount|type.*number" web/pages/GroupDetails.tsx -C 3

</function_calls>

Let me check if there's validation on the Input component:
<function_calls>

rg -n "required|disabled.*submit|validateAmount" web/pages/GroupDetails.tsx

</function_calls>

Let me examine the entire file to understand the structure better:
<function_calls>

cat web/pages/GroupDetails.tsx | head -100

</function_calls>

Let me check the file size and read it systematically:
<function_calls>

wc -l web/pages/GroupDetails.tsx && echo "---" && cat -n web/pages/GroupDetails.tsx | sed -n '200,270p'

</function_calls>

Now let me search for the input field that takes the amount:
<function_calls>

cat -n web/pages/GroupDetails.tsx | grep -A 10 -B 10 "amount"

</function_calls>

Let me extract more context around the form:
<function_calls>

cat -n web/pages/GroupDetails.tsx | sed -n '1,60p' && echo "..." && cat -n web/pages/GroupDetails.tsx | sed -n '250,350p'

</function_calls>

Let me check what type of Input component is being used:
<function_calls>

rg "from.*Input|import.*Input" web/pages/GroupDetails.tsx

</function_calls>

Let me look at the entire file in segments to find the form:
<function_calls>

grep -n "handleExpenseSubmit|<form|<Input|name="amount"" web/pages/GroupDetails.tsx

</function_calls>

Let me read a larger section to find the form rendering:
<function_calls>

tail -n +300 web/pages/GroupDetails.tsx | head -100

</function_calls>

Let me find all occurrences of amount-related state and inputs:
<function_calls>

rg -n "setAmount|amount\s*=" web/pages/GroupDetails.tsx

</function_calls>

Let me look at the entire file to map out its structure:
<function_calls>

cat web/pages/GroupDetails.tsx | python3 << 'EOF'
import sys
content = sys.stdin.read()
lines = content.split('\n')
for i, line in enumerate(lines, 1):
if any(keyword in line for keyword in ['handleExpenseSubmit', '<Input', 'name="amount"', 'required', 'setAmount']):
print(f"{i:4d}: {line}")
EOF


</function_calls>

Let me read the file directly:
<function_calls>

cat web/pages/GroupDetails.tsx

</function_calls>

Perfect! Now I have the full file. Let me analyze the specific function and context:
<function_calls>

cat -n web/pages/GroupDetails.tsx | sed -n '195,260p'

</function_calls>

Now let me search for where the amount input is rendered:
<function_calls>

cat -n web/pages/GroupDetails.tsx | grep -n "amount" -i

</function_calls>

Let me look at the JSX form section:
<function_calls>

cat -n web/pages/GroupDetails.tsx | sed -n '350,450p'

</function_calls>

Let me search for the expense modal form:
<function_calls>

cat -n web/pages/GroupDetails.tsx | sed -n '300,550p'

</function_calls>

Let me check the API endpoints to see if there's backend validation:
<function_calls>

rg "createExpense|updateExpense" --type ts --type tsx -A 5 2>/dev/null | head -40

</function_calls>

Let me search for these API functions:
<function_calls>

rg -n "export.*createExpense|const createExpense|function createExpense" 2>/dev/null

</function_calls>

Let me look for API/service files:
<function_calls>

find . -name "api" -o -name "service" 2>/dev/null | grep -E ".(ts|tsx|js)$"

</function_calls>

Let me check the imports in GroupDetails.tsx:
<function_calls>

head -20 web/pages/GroupDetails.tsx

</function_calls>

Let me look for the API service file:
<function_calls>

find . -path "./web/lib/" -o -path "./web/services/" -o -path "./lib/*" 2>/dev/null | head -20

</function_calls>

Let me check lib directory:
<function_calls>

git ls-files | grep -E "^.lib..(ts|tsx|js)$"

</function_calls>

Let me search for where createExpense is defined:
<function_calls>

rg "createExpense\s*=" 2>/dev/null

</function_calls>

Let me look at all files:
<function_calls>

git ls-files

</function_calls>

Let me check the api file:
<function_calls>

cat web/lib/api.ts 2>/dev/null | head -100

</function_calls>

Let me check if the file exists:
<function_calls>

ls -la web/lib/ 2>/dev/null

</function_calls>

Let me find all TypeScript/JavaScript files:
<function_calls>

find web -type f -name ".ts" -o -name ".tsx" 2>/dev/null

</function_calls>

Let me check the structure:
<function_calls>

ls -la web/

</function_calls>

Now let me look at the api file:
<function_calls>

cat web/api.ts

</function_calls>

Perfect! Now let me verify the expense submission and validation. Let me check around line 201-252 in GroupDetails.tsx again to see the full context:
<function_calls>

sed -n '190,270p' web/pages/GroupDetails.tsx | cat -n

</function_calls>

Now let me check if there's an Input component with validation:
<function_calls>

grep -n "Input|input" web/pages/GroupDetails.tsx | head -20

</function_calls>

Let me look at the entire render section:
<function_calls>

tail -n 150 web/pages/GroupDetails.tsx

</function_calls>

Let me check the return JSX for the form:
<function_calls>

sed -n '350,500p' web/pages/GroupDetails.tsx

</function_calls>

Let me find where the expense form is rendered by searching more systematically:
<function_calls>

grep -n "value={amount}" web/pages/GroupDetails.tsx

</function_calls>

Let me look at the entire component to find the form:
<function_calls>

wc -l web/pages/GroupDetails.tsx && sed -n '430,500p' web/pages/GroupDetails.tsx

</function_calls>

Let me check for input tags:
<function_calls>

grep -n "<input|<Input" web/pages/GroupDetails.tsx

</function_calls>

Let me look at lines around 440-470:
<function_calls>

sed -n '435,480p' web/pages/GroupDetails.tsx

</function_calls>

Now let me check the entire render return statement to understand the form structure:
<function_calls>

sed -n '330,550p' web/pages/GroupDetails.tsx | cat -n

</function_calls>

Now I can see the form! Let me analyze it more carefully. The amount input appears to be an HTML input element. Let me verify:
<function_calls>

sed -n '456,475p' web/pages/GroupDetails.tsx

</function_calls>

Perfect! Now I have all the context. Let me analyze this systematically:

  1. The input field at line 469 has: type="number" value={amount} onChange={(e) => setAmount(e.target.value)} required
  2. The handleExpenseSubmit function parses the amount but doesn't validate it
  3. The required attribute means the input cannot be submitted empty via browser form submission
  4. However, the form can be bypassed or amount could be an invalid value

Let me verify if there are any other validations:
<function_calls>

rg "numAmount" web/pages/GroupDetails.tsx

</function_calls>

Now let me confirm my analysis by checking the actual API behavior. Let me look at the backend:
<function_calls>

find . -type f -name ".ts" -o -name ".js" 2>/dev/null | xargs grep -l "expense" 2>/dev/null | grep -v node_modules

</function_calls>

Let me check if there's a backend:
<function_calls>

ls -la

</function_calls>

Let me look for any validation on the backend side by checking API responses:
<function_calls>

git ls-files | grep -v node_modules

</function_calls>

Let me check if there are more files in web folder:
<function_calls>

find web -type f

</function_calls>

Based on my verification of the codebase, I can now provide the rewritten review comment:


Add validation for expense amount to prevent NaN and non-positive values from being sent to the API.

handleExpenseSubmit parses amount with parseFloat() without validating the result. Although the input field has type="number" and required attributes (line 469), these don't guarantee a valid finite positive value—JavaScript can still allow invalid inputs, and direct programmatic calls could bypass form validation. If numAmount is NaN or ≤ 0, split calculations will produce NaN values sent to the backend, potentially causing unexpected behavior.

Add an explicit guard immediately after parsing:

     const handleExpenseSubmit = async (e: React.FormEvent) => {
         e.preventDefault();
         if (!id) return;

-        const numAmount = parseFloat(amount);
+        const numAmount = parseFloat(amount);
+        if (!Number.isFinite(numAmount) || numAmount <= 0) {
+            alert('Please enter a valid amount greater than 0');
+            return;
+        }
         let requestSplits: { userId: string; amount: number }[] = [];

Likely an incorrect or invalid review comment.

Comment on lines +25 to +27
const [friends, setFriends] = useState<Friend[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add loading state UI.

The loading state is managed but there's no corresponding UI feedback. Users see nothing during data fetch, then content suddenly appears. Add a loading skeleton or spinner.

+      {/* Loading State */}
+      {loading && (
+        <motion.div
+          initial={{ opacity: 0 }}
+          animate={{ opacity: 1 }}
+          className="col-span-full text-center py-20"
+        >
+          <div className={`animate-spin w-12 h-12 mx-auto mb-4 border-4 border-t-transparent ${isNeo ? 'border-black' : 'border-white/50'} rounded-full`} />
+          <p className="text-xl font-bold opacity-50">Loading friends...</p>
+        </motion.div>
+      )}

Place this before the Friends Grid section and conditionally render the grid only when !loading.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In web/pages/Friends.tsx around lines 25 to 27, the component tracks a loading
state but never renders any UI for it; add a visible loading indicator (spinner
or skeleton) that displays while loading is true and hide the Friends Grid until
loading is false. Implement a conditional render before the Friends Grid: if
(loading) return or render a placeholder container with accessible
spinner/skeleton markup and minimal styling, otherwise render the existing grid;
ensure the loading UI is keyboard/screen-reader accessible and does not depend
on fetched data.

Comment on lines +209 to +211
<button
type="button"
onClick={() => window.location.reload()}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider re-fetching data instead of full page reload.

Using window.location.reload() works but is a heavy-handed approach. Consider extracting fetchData and calling it directly for a smoother retry experience.

+  const fetchData = useCallback(async () => {
+    setLoading(true);
+    setError(null);
     // ... existing fetch logic
+  }, []);

   useEffect(() => {
     fetchData();
-  }, []);
+  }, [fetchData]);

   // In retry button:
-  onClick={() => window.location.reload()}
+  onClick={fetchData}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In web/pages/Friends.tsx around lines 209 to 211, the retry button currently
calls window.location.reload(), which is a full page reload; instead extract the
data-fetch logic into a reusable async function (e.g., fetchData) that sets
loading and error state and updates the component state with new results, then
replace the onClick handler to call that function to re-fetch the friends data
and handle success/error without reloading the page.

Comment on lines +225 to +233
{filteredFriends.length === 0 && !error ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="col-span-full text-center py-20 opacity-50"
>
<Users size={48} className="mx-auto mb-4 opacity-50" />
<p className="text-xl font-bold">No friends found</p>
</motion.div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty state displays during loading.

The condition filteredFriends.length === 0 && !error doesn't account for loading state. Users briefly see "No friends found" before data loads.

-          {filteredFriends.length === 0 && !error ? (
+          {filteredFriends.length === 0 && !error && !loading ? (
             <motion.div
               initial={{ opacity: 0 }}
               animate={{ opacity: 1 }}
               className="col-span-full text-center py-20 opacity-50"
             >
               <Users size={48} className="mx-auto mb-4 opacity-50" />
               <p className="text-xl font-bold">No friends found</p>
             </motion.div>
📝 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
{filteredFriends.length === 0 && !error ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="col-span-full text-center py-20 opacity-50"
>
<Users size={48} className="mx-auto mb-4 opacity-50" />
<p className="text-xl font-bold">No friends found</p>
</motion.div>
{filteredFriends.length === 0 && !error && !loading ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="col-span-full text-center py-20 opacity-50"
>
<Users size={48} className="mx-auto mb-4 opacity-50" />
<p className="text-xl font-bold">No friends found</p>
</motion.div>
🤖 Prompt for AI Agents
In web/pages/Friends.tsx around lines 225 to 233, the empty-state rendering uses
the condition filteredFriends.length === 0 && !error which does not account for
a loading state and causes "No friends found" to flash while data is being
fetched; update the conditional to also require that loading is false (e.g.,
filteredFriends.length === 0 && !error && !loading) so the empty message only
shows after loading completes (optionally render a loader or skeleton while
loading).

Comment on lines +302 to +307
<button type="button" className={`w-full mt-4 py-2 text-sm font-bold flex items-center justify-center gap-2 transition-colors ${isNeo
? 'bg-black text-white hover:bg-gray-800 rounded-none'
: 'bg-white/10 hover:bg-white/20 rounded-xl'
}`}>
View Details <ArrowRight size={14} />
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"View Details" button has no click handler.

The button is rendered but lacks an onClick handler, making it non-functional. Either implement navigation/action or remove the button until the feature is ready.

-                        <button type="button" className={`w-full mt-4 py-2 text-sm font-bold flex items-center justify-center gap-2 transition-colors ${isNeo
+                        <button
+                          type="button"
+                          onClick={() => { /* TODO: Navigate to friend details */ }}
+                          className={`w-full mt-4 py-2 text-sm font-bold flex items-center justify-center gap-2 transition-colors ${isNeo
                             ? 'bg-black text-white hover:bg-gray-800 rounded-none'
                             : 'bg-white/10 hover:bg-white/20 rounded-xl'
                           }`}>
                           View Details <ArrowRight size={14} />
                         </button>

Would you like me to open an issue to track implementing the friend details navigation?

📝 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
<button type="button" className={`w-full mt-4 py-2 text-sm font-bold flex items-center justify-center gap-2 transition-colors ${isNeo
? 'bg-black text-white hover:bg-gray-800 rounded-none'
: 'bg-white/10 hover:bg-white/20 rounded-xl'
}`}>
View Details <ArrowRight size={14} />
</button>
<button
type="button"
onClick={() => { /* TODO: Navigate to friend details */ }}
className={`w-full mt-4 py-2 text-sm font-bold flex items-center justify-center gap-2 transition-colors ${isNeo
? 'bg-black text-white hover:bg-gray-800 rounded-none'
: 'bg-white/10 hover:bg-white/20 rounded-xl'
}`}>
View Details <ArrowRight size={14} />
</button>
🤖 Prompt for AI Agents
In web/pages/Friends.tsx around lines 302 to 307 the "View Details" button is
rendered without an onClick handler making it non-functional; either wire it to
navigate to the friend's detail view (e.g., use next/router push or wrap with
<Link> to `/friends/[id]` passing the correct friend id from scope) or remove
the button until implemented; if adding navigation, call router.push or Link
with the friend's id, ensure the id is available in the component props/state,
and add an aria-label for accessibility.

Comment on lines +29 to +35
interface Settlement {
fromUserId: string;
fromUserName: string;
toUserId: string;
toUserName: string;
amount: number;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify local Settlement type vs shared Settlement in web/types.ts.

There’s a locally defined interface Settlement whose shape (fromUserId, toUserId, names, amount) differs from the exported Settlement in web/types.ts (with _id, payerId, payeeId, status, etc.). This name collision is confusing and makes it easy to use the wrong type by mistake.

Consider either:

  • Reusing the shared Settlement type if the API shape is actually the same, or
  • Renaming this local type to something like OptimizedSettlement (and documenting that it matches getOptimizedSettlements’ response).
🤖 Prompt for AI Agents
In web/pages/GroupDetails.tsx around lines 29 to 35, there is a local interface
named Settlement whose shape diverges from the shared Settlement in web/types.ts
which causes a confusing name collision; either import and use the shared
Settlement type from web/types.ts if the API response actually matches that
definition, or rename the local interface to a distinct name like
OptimizedSettlement (and add a brief comment indicating it matches the
getOptimizedSettlements response) and update all local usages to the new name to
avoid accidental mixing of types.

Comment on lines +125 to +136
const copyToClipboard = () => {
if (group?.joinCode) {
navigator.clipboard.writeText(group.joinCode)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(() => {
alert('Failed to copy to clipboard');
});
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Good clipboard error handling; consider guarding for navigator.clipboard availability.

Nice upgrade to handle navigator.clipboard.writeText via promises and show a failure alert; this is much more robust than a bare call.

One additional hardening step is to guard against environments where navigator.clipboard is undefined (e.g., non-secure contexts) before calling writeText:

-        if (group?.joinCode) {
-            navigator.clipboard.writeText(group.joinCode)
+        if (group?.joinCode && navigator.clipboard?.writeText) {
+            navigator.clipboard.writeText(group.joinCode)
                 .then(() => {
                     setCopied(true);
                     setTimeout(() => setCopied(false), 2000);
                 })
                 .catch(() => {
                     alert('Failed to copy to clipboard');
                 });
         }

This prevents a runtime error in older/unsupported browsers.

🤖 Prompt for AI Agents
In web/pages/GroupDetails.tsx around lines 125 to 136, the copyToClipboard
function calls navigator.clipboard.writeText without guarding against
environments where navigator.clipboard is undefined; add a guard that checks if
navigator and navigator.clipboard exist before calling writeText, and if not
available, fall back to a graceful alternative (e.g., show an alert/instruction
to manually copy or use a legacy fallback like creating a temporary textarea and
execCommand('copy')), ensure you return early when clipboard API is missing and
preserve the existing promise handling and copied state logic for the successful
path.

Comment on lines +456 to +467
<motion.div
layout
key={expense._id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
whileHover={{ scale: 1.02 }}
onClick={() => openEditExpense(expense)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openEditExpense(expense); } }}
tabIndex={0}
role="button"
aria-label={`Expense: ${expense.description}, ${group.currency} ${expense.amount.toFixed(2)}`}
className={`p-5 flex items-center gap-5 cursor-pointer group relative overflow-hidden ${style === THEMES.NEOBRUTALISM
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Use a semantic button instead of motion.div with role="button".

The expense card is keyboard-focusable and has role="button", and Biome suggests using a semantic <button> instead. This improves a11y and removes the need for manual tabIndex/role.

You can switch to motion.button:

-                        expenses.map((expense, idx) => (
-                            <motion.div
+                        expenses.map((expense, idx) => (
+                            <motion.button
                                 layout
                                 key={expense._id}
+                                type="button"
                                 initial={{ opacity: 0, scale: 0.95 }}
                                 animate={{ opacity: 1, scale: 1 }}
                                 whileHover={{ scale: 1.02 }}
                                 onClick={() => openEditExpense(expense)}
                                 onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openEditExpense(expense); } }}
-                                tabIndex={0}
-                                role="button"
                                 aria-label={`Expense: ${expense.description}, ${group.currency} ${expense.amount.toFixed(2)}`}
                                 className={`p-5 flex items-center gap-5 cursor-pointer group relative overflow-hidden ${style === THEMES.NEOBRUTALISM
                                     ? 'bg-white border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] rounded-none'
                                     : 'bg-white/5 border border-white/10 rounded-2xl backdrop-blur-sm hover:bg-white/10 transition-all'
                                     }`}
-                            >
+                            >

(Remember to update the closing tag to </motion.button>.)

🧰 Tools
🪛 Biome (2.1.2)

[error] 465-465: The elements with this role can be changed to the following elements:

For examples and more information, see WAI-ARIA Roles

(lint/a11y/useSemanticElements)

🤖 Prompt for AI Agents
In web/pages/GroupDetails.tsx around lines 456 to 467, the expense card uses a
keyboard-focusable motion.div with role="button", tabIndex and a manual
onKeyDown handler; replace it with a semantic motion.button (change opening and
closing tags to motion.button/motion.button) to improve accessibility, remove
role and tabIndex and the custom onKeyDown, add type="button" to prevent form
submit, keep the existing layout, key, initial/animate/whileHover props, onClick
handler, aria-label and className intact so visual/animation behavior is
preserved while relying on the native button keyboard behavior.

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