Skip to content

Conversation

@google-labs-jules
Copy link
Contributor

@google-labs-jules google-labs-jules bot commented Jan 21, 2026

This PR introduces a complete Confirmation Dialog system to the Web application, replacing the native and intrusive window.confirm dialogs with a custom, accessible, and themable UI.

Changes:

  1. New Component: web/components/ui/ConfirmDialog.tsx - A specialized modal for confirmations with support for 'danger', 'warning', and 'info' variants.
  2. New Context: web/contexts/ConfirmContext.tsx - Provides a useConfirm hook that returns a Promise, allowing for cleaner async/await usage in components.
  3. Integration: Wrapped the application in ConfirmProvider within web/App.tsx.
  4. Refactor: Updated web/pages/GroupDetails.tsx to use the new system for all destructive actions.
  5. Accessibility: Improved the existing Modal component to include proper ARIA attributes, ensuring it is correctly identified by screen readers.

Verification:

  • Verified via Playwright (script created, run, and then cleaned up) that the dialog appears, captures focus, and correctly resolves the promise upon user interaction.
  • Verified that the integration with existing Modal styles works for both themes.

PR created automatically by Jules for task 12669270586709539879 started by @Devasy23

Summary by CodeRabbit

  • New Features

    • Introduced a confirmation dialog system for destructive actions (delete, remove members, leave groups), replacing native browser dialogs with themed, accessible modals.
  • Improvements

    • Enhanced modal accessibility with ARIA attributes to ensure proper screen reader detection and navigation.

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

- Replaces `window.confirm` with a custom, accessible `ConfirmDialog` component.
- Implements `ConfirmContext` for asynchronous confirmation handling using Promises.
- Adds `ConfirmProvider` to `App.tsx` to enable global usage.
- Updates `GroupDetails.tsx` to use `useConfirm` for destructive actions (Delete Group, Delete Expense, Leave Group, Remove Member).
- Enhances `Modal.tsx` accessibility by adding `role="dialog"`, `aria-modal="true"`, and `aria-labelledby`.
- Supports both Neobrutalism and Glassmorphism themes.
@google-labs-jules
Copy link
Contributor Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@netlify
Copy link

netlify bot commented Jan 21, 2026

Deploy Preview for split-but-wiser ready!

Name Link
🔨 Latest commit 5e40f93
🔍 Latest deploy log https://app.netlify.com/projects/split-but-wiser/deploys/69713188cb51060008e53202
😎 Deploy Preview https://deploy-preview-256--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.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Walkthrough

Introduces a promise-based Confirmation Dialog System replacing window.confirm with an accessible modal. Adds ConfirmDialog component and ConfirmContext provider with useConfirm hook. Enhances Modal component with ARIA attributes. Integrates confirmations into GroupDetails for destructive actions and updates documentation accordingly.

Changes

Cohort / File(s) Summary
Confirmation Dialog System
web/components/ui/ConfirmDialog.tsx, web/contexts/ConfirmContext.tsx
Implements promise-based modal confirmation with ConfirmVariant types (danger/warning/info), theme-aware styling, useConfirm hook, and internal state management via ConfirmProvider
UI Accessibility Improvements
web/components/ui/Modal.tsx
Adds ARIA attributes (role="dialog", aria-modal="true", aria-labelledby, id="modal-title") and aria-label for close button to improve screen reader support
Provider Integration
web/App.tsx
Wraps application component tree with ConfirmProvider to enable dialog system across all routes and components
Feature Implementation
web/pages/GroupDetails.tsx
Replaces synchronous window.confirm with asynchronous useConfirm-based confirmations for destructive operations (delete expense, delete group, leave group, remove member)
Documentation Updates
.Jules/changelog.md, .Jules/knowledge.md, .Jules/todo.md
Reflects Confirmation Dialog System implementation, usage patterns, accessibility improvements, and task migration between priority levels

Suggested reviewers

  • vrajpatelll
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[UX] Confirmation Dialog System' directly and clearly reflects the main change: introducing a custom confirmation dialog system to replace native window.confirm dialogs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

@Devasy Devasy marked this pull request as ready for review January 23, 2026 16:19
@Devasy Devasy requested a review from vrajpatelll as a code owner January 23, 2026 16:19
@Devasy
Copy link
Owner

Devasy commented Jan 23, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

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

⚠️ Outside diff range comments (1)
web/pages/GroupDetails.tsx (1)

366-393: Unsettled balance check happens after confirmation.

The check for unsettled balances (lines 379-385) occurs after the user confirms removal. Consider validating this before showing the confirmation dialog to avoid a confusing experience where the user confirms but the action still fails.

Move validation before confirmation
     const handleKickMember = async (memberId: string, memberName: string) => {
         if (!id || !isAdmin) return;
         if (memberId === user?._id) return;
 
+        const hasUnsettled = settlements.some(
+            s => (s.fromUserId === memberId || s.toUserId === memberId) && (s.amount || 0) > 0
+        );
+        if (hasUnsettled) {
+            addToast('Cannot remove: This member has unsettled balances in the group.', 'error');
+            return;
+        }
+
         const confirmed = await confirm({
             title: 'Remove Member',
             description: `Are you sure you want to remove ${memberName} from the group?`,
             confirmText: 'Remove',
             variant: 'danger'
         });
 
         if (confirmed) {
             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();
                 addToast(`Removed ${memberName} from group`, 'success');
             } catch (err: any) {
                 addToast(err.response?.data?.detail || "Failed to remove member", 'error');
             }
         }
     };
🤖 Fix all issues with AI agents
In `@web/components/ui/ConfirmDialog.tsx`:
- Around line 31-32: The destructured but unused variable `mode` from useTheme()
should be removed to eliminate the unused variable warning: update the
destructuring in the ConfirmDialog component (where useTheme() is called) to
only pull `style` (e.g., change `const { style, mode } = useTheme()` to `const {
style } = useTheme()`), leaving the rest of the logic that uses `style`/`isNeo`
(`THEMES.NEOBRUTALISM`, `isNeo`) untouched.
- Around line 71-79: The ConfirmDialog currently sets autoFocus on the Confirm
button which can cause accidental destructive confirmations; update the footer
so that autoFocus is applied conditionally: if getButtonVariant() returns a
destructive variant (e.g., "danger" or "warning") move the autoFocus prop from
the confirm Button (onConfirm) to the cancel Button (onCancel), otherwise keep
autoFocus on the confirm Button; reference ConfirmDialog, getButtonVariant,
onConfirm, onCancel, cancelText and confirmText when making this change.

In `@web/components/ui/Modal.tsx`:
- Around line 68-70: The close button in Modal.tsx currently can act as a submit
button when placed inside forms; update the <button> element that calls onClose
(the button rendering the X icon) to include an explicit type="button" attribute
so it never triggers form submission, keeping the onClose handler and existing
classes/aria-label unchanged.
- Line 46: The Modal component currently hardcodes id="modal-title", which can
collide when multiple modals render; update the Modal (in
web/components/ui/Modal.tsx) to generate a unique id (e.g., call React's useId()
at top of the Modal component), assign that generated id to the dialog's
aria-labelledby attribute and to the modal title element (the element that
currently has id="modal-title"); add the necessary import for useId and replace
the hardcoded string with the generated id so each Modal instance gets a unique,
matching aria-labelledby/title id pair.

In `@web/contexts/ConfirmContext.tsx`:
- Around line 26-32: The current confirm function overwrites the previous
pending promise's resolver, leaving it unresolved; update confirm to first check
for an existing resolver (e.g., resolveRef.current or the existing value held by
setResolveRef) and settle it (call the previous resolve with false or call a
stored reject) before storing the new resolve and returning the new Promise;
specifically, in confirm, before setResolveRef(() => resolve) call the existing
resolver if present to resolve(false) (or reject) so the previous promise is not
left hanging, then proceed to setOptions, setIsOpen, and set the new resolver.

Comment on lines +31 to +32
const { style, mode } = useTheme();
const isNeo = 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

Remove unused mode variable.

The mode variable is destructured from useTheme() but is never used in this component.

♻️ Suggested fix
-  const { style, mode } = useTheme();
+  const { style } = useTheme();
📝 Committable suggestion

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

Suggested change
const { style, mode } = useTheme();
const isNeo = style === THEMES.NEOBRUTALISM;
const { style } = useTheme();
const isNeo = style === THEMES.NEOBRUTALISM;
🤖 Prompt for AI Agents
In `@web/components/ui/ConfirmDialog.tsx` around lines 31 - 32, The destructured
but unused variable `mode` from useTheme() should be removed to eliminate the
unused variable warning: update the destructuring in the ConfirmDialog component
(where useTheme() is called) to only pull `style` (e.g., change `const { style,
mode } = useTheme()` to `const { style } = useTheme()`), leaving the rest of the
logic that uses `style`/`isNeo` (`THEMES.NEOBRUTALISM`, `isNeo`) untouched.

Comment on lines +71 to +79
footer={
<>
<Button variant="ghost" onClick={onCancel}>
{cancelText}
</Button>
<Button variant={getButtonVariant()} onClick={onConfirm} autoFocus>
{confirmText}
</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

Consider focusing the Cancel button for destructive actions.

The autoFocus on the Confirm button (line 76) may lead to accidental confirmations for danger and warning variants when users press Enter. Accessibility best practices recommend focusing the safer action (Cancel) for destructive dialogs.

🛡️ Suggested fix
      footer={
        <>
-          <Button variant="ghost" onClick={onCancel}>
+          <Button variant="ghost" onClick={onCancel} autoFocus={variant === 'danger' || variant === 'warning'}>
            {cancelText}
          </Button>
-          <Button variant={getButtonVariant()} onClick={onConfirm} autoFocus>
+          <Button variant={getButtonVariant()} onClick={onConfirm} autoFocus={variant === 'info'}>
            {confirmText}
          </Button>
        </>
      }
📝 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
footer={
<>
<Button variant="ghost" onClick={onCancel}>
{cancelText}
</Button>
<Button variant={getButtonVariant()} onClick={onConfirm} autoFocus>
{confirmText}
</Button>
</>
footer={
<>
<Button variant="ghost" onClick={onCancel} autoFocus={variant === 'danger' || variant === 'warning'}>
{cancelText}
</Button>
<Button variant={getButtonVariant()} onClick={onConfirm} autoFocus={variant === 'info'}>
{confirmText}
</Button>
</>
}
🤖 Prompt for AI Agents
In `@web/components/ui/ConfirmDialog.tsx` around lines 71 - 79, The ConfirmDialog
currently sets autoFocus on the Confirm button which can cause accidental
destructive confirmations; update the footer so that autoFocus is applied
conditionally: if getButtonVariant() returns a destructive variant (e.g.,
"danger" or "warning") move the autoFocus prop from the confirm Button
(onConfirm) to the cancel Button (onCancel), otherwise keep autoFocus on the
confirm Button; reference ConfirmDialog, getButtonVariant, onConfirm, onCancel,
cancelText and confirmText when making this change.

<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" role="dialog" aria-modal="true" aria-labelledby="modal-title">
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

Hardcoded id="modal-title" may conflict if multiple modals render simultaneously.

If two Modal instances are open at the same time, both will have elements with id="modal-title", violating the HTML requirement that IDs be unique. Consider generating a unique ID (e.g., using useId() from React 19) and passing it to both aria-labelledby and the title element.

Suggested approach using React's useId
+import { useId } from 'react';
 
 export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, footer }) => {
   const { style, mode } = useTheme();
+  const titleId = useId();
 
   // ... variants ...
 
   return (
     <AnimatePresence>
       {isOpen && (
-        <div className="fixed inset-0 z-50 flex items-center justify-center p-4" role="dialog" aria-modal="true" aria-labelledby="modal-title">
+        <div className="fixed inset-0 z-50 flex items-center justify-center p-4" role="dialog" aria-modal="true" aria-labelledby={titleId}>
           {/* ... */}
-              <h3 id="modal-title" className={...}>{title}</h3>
+              <h3 id={titleId} className={...}>{title}</h3>
🤖 Prompt for AI Agents
In `@web/components/ui/Modal.tsx` at line 46, The Modal component currently
hardcodes id="modal-title", which can collide when multiple modals render;
update the Modal (in web/components/ui/Modal.tsx) to generate a unique id (e.g.,
call React's useId() at top of the Modal component), assign that generated id to
the dialog's aria-labelledby attribute and to the modal title element (the
element that currently has id="modal-title"); add the necessary import for useId
and replace the hardcoded string with the generated id so each Modal instance
gets a unique, matching aria-labelledby/title id pair.

Comment on lines +68 to 70
<button onClick={onClose} className="hover:rotate-90 transition-transform duration-200" aria-label="Close modal">
<X size={24} />
</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

Add type="button" to prevent unintended form submission.

The close button lacks an explicit type attribute. Buttons default to type="submit", which can cause unintended form submissions when the Modal wraps form content.

Proposed fix
-              <button onClick={onClose} className="hover:rotate-90 transition-transform duration-200" aria-label="Close modal">
+              <button type="button" onClick={onClose} className="hover:rotate-90 transition-transform duration-200" aria-label="Close modal">
                 <X size={24} />
               </button>
📝 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 onClick={onClose} className="hover:rotate-90 transition-transform duration-200" aria-label="Close modal">
<X size={24} />
</button>
<button type="button" onClick={onClose} className="hover:rotate-90 transition-transform duration-200" aria-label="Close modal">
<X size={24} />
</button>
🧰 Tools
🪛 Biome (2.1.2)

[error] 68-68: 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)

🤖 Prompt for AI Agents
In `@web/components/ui/Modal.tsx` around lines 68 - 70, The close button in
Modal.tsx currently can act as a submit button when placed inside forms; update
the <button> element that calls onClose (the button rendering the X icon) to
include an explicit type="button" attribute so it never triggers form
submission, keeping the onClose handler and existing classes/aria-label
unchanged.

Comment on lines +26 to +32
const confirm = useCallback((options: ConfirmOptions) => {
setOptions(options);
setIsOpen(true);
return new Promise<boolean>((resolve) => {
setResolveRef(() => resolve);
});
}, []);
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 handling concurrent confirm() calls.

If confirm() is called while a dialog is already open, the previous promise's resolve function is overwritten, leaving the earlier promise unresolved indefinitely. While unlikely in typical usage, this could cause issues if triggered programmatically.

Optional: Reject previous promise on new confirm call
   const confirm = useCallback((options: ConfirmOptions) => {
+    // Resolve any pending promise as false before opening new dialog
+    if (resolveRef) {
+      resolveRef(false);
+      setResolveRef(null);
+    }
     setOptions(options);
     setIsOpen(true);
     return new Promise<boolean>((resolve) => {
       setResolveRef(() => resolve);
     });
-  }, []);
+  }, [resolveRef]);
🤖 Prompt for AI Agents
In `@web/contexts/ConfirmContext.tsx` around lines 26 - 32, The current confirm
function overwrites the previous pending promise's resolver, leaving it
unresolved; update confirm to first check for an existing resolver (e.g.,
resolveRef.current or the existing value held by setResolveRef) and settle it
(call the previous resolve with false or call a stored reject) before storing
the new resolve and returning the new Promise; specifically, in confirm, before
setResolveRef(() => resolve) call the existing resolver if present to
resolve(false) (or reject) so the previous promise is not left hanging, then
proceed to setOptions, setIsOpen, and set the new resolver.

@Devasy
Copy link
Owner

Devasy commented Jan 24, 2026

@jules In @web/components/ui/ConfirmDialog.tsx: - Around line 31-32: The destructured but unused variable mode from useTheme() should be removed to eliminate the unused variable warning: update the destructuring in the ConfirmDialog component (where useTheme() is called) to only pull style (e.g., change const { style, mode } = useTheme() to const { style } = useTheme()), leaving the rest of the logic that uses style/isNeo (THEMES.NEOBRUTALISM, isNeo) untouched. - Around line 71-79: The ConfirmDialog currently sets autoFocus on the Confirm button which can cause accidental destructive confirmations; update the footer so that autoFocus is applied conditionally: if getButtonVariant() returns a destructive variant (e.g., "danger" or "warning") move the autoFocus prop from the confirm Button (onConfirm) to the cancel Button (onCancel), otherwise keep autoFocus on the confirm Button; reference ConfirmDialog, getButtonVariant, onConfirm, onCancel, cancelText and confirmText when making this change. In @web/components/ui/Modal.tsx: - Around line 68-70: The close button in Modal.tsx currently can act as a submit button when placed inside forms; update the element that calls onClose (the button rendering the X icon) to include an explicit type="button" attribute so it never triggers form submission, keeping the onClose handler and existing classes/aria-label unchanged. - Line 46: The Modal component currently hardcodes id="modal-title", which can collide when multiple modals render; update the Modal (in web/components/ui/Modal.tsx) to generate a unique id (e.g., call React's useId() at top of the Modal component), assign that generated id to the dialog's aria-labelledby attribute and to the modal title element (the element that currently has id="modal-title"); add the necessary import for useId and replace the hardcoded string with the generated id so each Modal instance gets a unique, matching aria-labelledby/title id pair. In @web/contexts/ConfirmContext.tsx: - Around line 26-32: The current confirm function overwrites the previous pending promise's resolver, leaving it unresolved; update confirm to first check for an existing resolver (e.g., resolveRef.current or the existing value held by setResolveRef) and settle it (call the previous resolve with false or call a stored reject) before storing the new resolve and returning the new Promise; specifically, in confirm, before setResolveRef(() => resolve) call the existing resolver if present to resolve(false) (or reject) so the previous promise is not left hanging, then proceed to setOptions, setIsOpen, and set the new resolver.

366-393: Unsettled balance check happens after confirmation.

The check for unsettled balances (lines 379-385) occurs after the user confirms removal. Consider validating this before showing the confirmation dialog to avoid a confusing experience where the user confirms but the action still fails.

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