Skip to content

feat: add in-app notification system with welcome message (#98)#100

Merged
hrx01-dev merged 2 commits into
mainfrom
devin/1782065796-notification-welcome-trigger
Jun 21, 2026
Merged

feat: add in-app notification system with welcome message (#98)#100
hrx01-dev merged 2 commits into
mainfrom
devin/1782065796-notification-welcome-trigger

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the full in-app notification system requested in #98, plus a welcome notification on signup (requested in PR #99 review).

Supersedes #99 (blocked by branch protection rules on subsequent pushes).

Architecture

src/dashboard/notifications/
├── types.ts                      # Notification & NotificationPreferences interfaces
├── notificationService.ts        # Firestore CRUD: subscribe, paginate, mark-read, preferences
├── notificationTriggers.ts       # Typed helpers to create notifications for events
├── NotificationContextObject.ts  # React context definition (split for fast-refresh)
├── NotificationContext.tsx       # NotificationProvider — real-time Firestore listeners
├── useNotifications.ts           # useNotifications() hook
├── NotificationBell.tsx          # Bell icon + dropdown panel in top bar
├── NotificationCenter.tsx        # /dashboard/notifications — full-page list
├── NotificationPreferences.tsx   # /dashboard/notification-preferences — toggle categories/channels
└── index.ts                      # Barrel exports

Key decisions

  • Real-time via onSnapshot: NotificationProvider subscribes to Firestore — notifications update instantly without refresh.
  • Context split: Separates createContext, Provider, and useNotifications into three files to satisfy react-refresh/only-export-components.
  • Top bar bell visible globally: DashboardLayout header includes <NotificationBell /> with animated unread badge and dropdown.
  • Client-side filtering in center: NotificationCenter filters/searches/paginates over the real-time snapshot; fetchNotificationsPage is available for server-side cursor pagination.
  • Trigger utilities: notificationTriggers.ts exports typed helpers (notifyProjectCreated, notifyPaymentReceived, notifyWelcome, etc.).
  • Welcome notification on signup: Both email and Google signup flows in SignUp.tsx now call notifyWelcome(user.uid, name) after successful registration.
  • Preferences: Users toggle category subscriptions and delivery channels, persisted to notificationPreferences/{userId} in Firestore.

Routes added

  • /dashboard/notificationsNotificationCenter
  • /dashboard/notification-preferencesNotificationPreferences

Link to Devin session: https://app.devin.ai/sessions/0f66026af44c4180b0c1a5adefe5832b
Requested by: @hrx01-dev

Summary by CodeRabbit

  • New Features
    • Added a notification system with a dashboard bell icon displaying unread count
    • Introduced a notifications center page with search, filtering, and pagination
    • Added notification preferences page for managing notification categories and delivery channels
    • Implemented welcome notifications upon user sign-up and login
    • Real-time notifications across project, payment, account, and system categories with mark as read functionality

hrx01-dev and others added 2 commits June 21, 2026 18:03
- Notification types, Firestore service with real-time listeners
- NotificationProvider context with subscribe/mark-read/preferences
- NotificationBell dropdown with unread badge in dashboard top bar
- NotificationCenter page at /dashboard/notifications with filters, search, pagination
- NotificationPreferences page at /dashboard/notification-preferences
- Notification trigger utilities for project, payment, account, and admin events
- Routes and sidebar navigation integration

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Calls notifyWelcome() after both email and Google sign-up flows,
creating a welcome notification in the user's notification center.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a complete in-app notification system: domain types, a Firestore service layer, event trigger helpers, a React context/provider with real-time subscriptions, a NotificationBell header dropdown, a NotificationCenter page, a NotificationPreferences page, dashboard layout/route wiring, and a welcome notification fired on sign-up.

Changes

In-App Notification System

Layer / File(s) Summary
Domain types, context object, and service layer
src/dashboard/notifications/types.ts, src/dashboard/notifications/NotificationContextObject.ts, src/dashboard/notifications/notificationService.ts
Defines NotificationType, NotificationCategory, Notification, NotificationPreferences, and DEFAULT_PREFERENCES. Exports NotificationContext with default no-op values. Implements all Firestore service functions: real-time subscription, paginated fetch, markAsRead, markAllAsRead, createNotification, subscribeToPreferences, and updatePreferences.
Notification trigger helpers
src/dashboard/notifications/notificationTriggers.ts
Adds a shared trigger helper and 14 event-specific notify* functions covering project, payment, account, and admin event categories, each constructing typed payloads and calling createNotification.
NotificationProvider and useNotifications hook
src/dashboard/notifications/NotificationContext.tsx, src/dashboard/notifications/useNotifications.ts, src/dashboard/notifications/index.ts
NotificationProvider manages notification and preference state, sets up/tears down real-time Firestore subscriptions on auth changes, computes unreadCount, and exposes memoized async action handlers via context. useNotifications returns the context value.
NotificationBell dropdown
src/dashboard/notifications/NotificationBell.tsx
Animated dropdown attached to a bell icon with unread badge, shows up to 8 recent notifications, supports outside-click close, per-item read-on-click, mark-all-read, and a footer link to the full notification center.
NotificationCenter full-page UI
src/dashboard/notifications/NotificationCenter.tsx
Full notifications page with search, category filter, read/unread filter, useMemo-derived filtered list, pagination controls, loading skeleton, empty-state, and per-row mark-as-read plus actionUrl navigation.
NotificationPreferences page
src/dashboard/notifications/NotificationPreferences.tsx
Preferences UI with ToggleRow components for four notification categories and three delivery channels, local state diff detection via JSON comparison, async save with timed success indicator, and a Save button disabled when no changes exist.
Dashboard layout wiring and route registration
src/dashboard/components/DashboardLayout.tsx, src/app/App.tsx
Wraps DashboardLayout in NotificationProvider, adds NotificationBell to the consolidated header, extends NAV_ITEMS with a Notifications entry, and registers /dashboard/notifications and /dashboard/notification-preferences routes.
Welcome notification on sign-up
src/Firebase/SignUp.tsx
Calls notifyWelcome with the authenticated user's display name or email fallback after both email/password sign-up and Google sign-in succeed, before navigating to /dashboard.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SignUp
  participant notificationTriggers
  participant notificationService
  participant Firestore
  participant NotificationBell

  rect rgba(70, 130, 180, 0.5)
    Note over User,Firestore: Sign-up flow
    User->>SignUp: submit credentials / Google sign-in
    SignUp->>notificationTriggers: notifyWelcome(userId, name)
    notificationTriggers->>notificationService: createNotification(payload)
    notificationService->>Firestore: addDoc to notifications collection
  end

  rect rgba(60, 179, 113, 0.5)
    Note over NotificationBell,Firestore: Real-time notification delivery
    NotificationBell->>notificationService: subscribeToNotifications(userId, cb)
    notificationService->>Firestore: onSnapshot(query)
    Firestore-->>notificationService: new notification snapshot
    notificationService-->>NotificationBell: cb(Notification[]) → renders badge + dropdown item
  end

  rect rgba(255, 165, 0, 0.5)
    Note over User,Firestore: Mark all as read
    User->>NotificationBell: click "Mark all read"
    NotificationBell->>notificationService: markAllAsRead(userId)
    notificationService->>Firestore: writeBatch isRead=true
    Firestore-->>notificationService: commit ack
    notificationService-->>NotificationBell: updated snapshot clears badge
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Add In-App Notification System #98: This PR implements the complete in-app notification system described in the issue, including the bell component, notification center, real-time Firestore subscriptions, trigger helpers (including welcome notification), and user preferences.

Possibly related PRs

  • hrx01-dev/Servio#51: Modifies the same SignUp.tsx email/password and Google sign-in success handlers that this PR extends with notifyWelcome.
  • hrx01-dev/Servio#68: Also modifies SignUp.tsx post-signup navigation flow to /dashboard, directly overlapping with the handlers changed in this PR.

Suggested reviewers

  • hrx01-dev

Poem

🐰 Hop hop, a bell now rings with glee,
Firestore streams each note to me!
A welcome chime on sign-up day,
Mark-all-read clears the queue away.
Preferences saved, the bunny cheers —
Notifications! The feature appears! 🔔

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the primary change: adding an in-app notification system with welcome message integration, which aligns with the substantial feature implementation across multiple files and components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1782065796-notification-welcome-trigger

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

@github-actions

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit af49c79):

https://servio-0--pr100-devin-1782065796-not-sm8znyqg.web.app

(expires Sun, 28 Jun 2026 18:18:07 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 15915abb5951eb298a844eda460b24f444d93a69

@hrx01-dev hrx01-dev self-requested a review June 21, 2026 18:19
@hrx01-dev hrx01-dev self-assigned this Jun 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/dashboard/notifications/NotificationCenter.tsx`:
- Around line 93-99: The Card component in NotificationCenter.tsx with the
onClick handler is only accessible via pointer clicks, blocking keyboard users.
Make the Card keyboard-activatable by adding a tabIndex attribute to make it
focusable, adding an onKeyDown event handler to detect Enter or Space key
presses and trigger the same handleClick logic, and optionally adding a role
attribute to indicate the component is interactive. This ensures both pointer
and keyboard users can open notifications and perform actions.

In `@src/dashboard/notifications/NotificationPreferences.tsx`:
- Around line 58-63: The `localPrefs` state is initialized only once with the
`preferences` prop, but when `preferences` changes asynchronously (such as when
preferences are loaded from the backend), the `localPrefs` state is not updated,
causing the UI to display stale defaults and false unsaved changes. Add a
useEffect hook that watches `preferences` as a dependency and updates
`localPrefs` whenever the `preferences` prop changes, ensuring the local state
stays synchronized with the async-loaded preferences.
- Around line 65-73: The handleSave function in NotificationPreferences.tsx
calls setSaved(true) after awaiting updatePreferences, but since the
updatePreferences function in NotificationContext.tsx logs and swallows errors
without rethrowing, the success state is set even when persistence fails. Modify
the updatePreferences function in NotificationContext.tsx to rethrow the error
after logging it, then update the try-catch block in handleSave to properly
handle the caught error so that setSaved(true) only executes when the update
actually succeeds.
- Around line 43-52: The Switch component in the notification preference item
lacks explicit association with its Label, making it inaccessible to screen
readers. Add a unique id attribute to the Label component and then link the
Switch component to that id using the aria-labelledby attribute to establish the
proper accessible relationship between the label and the switch toggle.

In `@src/dashboard/notifications/notificationService.ts`:
- Around line 154-161: The updatePreferences function contains an unnecessary
dynamic import of setDoc from firebase/firestore inside the function body. Since
setDoc is already imported at the top of the file from the same
firebase/firestore package, remove the dynamic import statement and simply use
the already-imported setDoc directly in the setDoc call within the function.
This will eliminate the redundant import and keep the code consistent with the
rest of the codebase.
- Around line 113-127: The markAllAsRead function performs a query on the
notifications collection with two where clauses filtering by userId and isRead
fields, which requires a composite index in Firestore. Add a composite index
configuration to firestore.indexes.json for the notifications collection with
fields userId (ascending) and isRead (ascending) to ensure the query on the
NOTIFICATIONS_COLLECTION will execute without runtime errors.

In `@src/Firebase/SignUp.tsx`:
- Around line 264-267: The current implementation calls notifyWelcome for all
users who sign in via the GoogleAuthProvider, including returning users. To fix
this, use getAdditionalUserInfo from Firebase to check if the user is new before
sending the welcome notification. Destructure the result of signInWithPopup to
include the additional user info, then check the isNewUser property from
getAdditionalUserInfo before calling notifyWelcome. Only invoke notifyWelcome
when isNewUser is true to prevent duplicate notifications for returning users.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad755d63-0d50-44f8-b2fe-5ac86c8df873

📥 Commits

Reviewing files that changed from the base of the PR and between 81bc69f and af49c79.

📒 Files selected for processing (13)
  • src/Firebase/SignUp.tsx
  • src/app/App.tsx
  • src/dashboard/components/DashboardLayout.tsx
  • src/dashboard/notifications/NotificationBell.tsx
  • src/dashboard/notifications/NotificationCenter.tsx
  • src/dashboard/notifications/NotificationContext.tsx
  • src/dashboard/notifications/NotificationContextObject.ts
  • src/dashboard/notifications/NotificationPreferences.tsx
  • src/dashboard/notifications/index.ts
  • src/dashboard/notifications/notificationService.ts
  • src/dashboard/notifications/notificationTriggers.ts
  • src/dashboard/notifications/types.ts
  • src/dashboard/notifications/useNotifications.ts

Comment on lines +93 to +99
<Card
className={`border-gray-200 dark:border-slate-800 bg-white dark:bg-slate-900 cursor-pointer hover:shadow-md transition-shadow ${
!notification.isRead ? "ring-1 ring-indigo-200 dark:ring-indigo-800" : ""
}`}
onClick={handleClick}
>
<CardContent className="flex items-start gap-4 py-4">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make rows keyboard-activatable, not pointer-only.

Line [93]-Line [99] wires interaction through onClick only. This can block keyboard users from opening actionable notifications or marking them read.

Suggested fix
       <Card
         className={`border-gray-200 dark:border-slate-800 bg-white dark:bg-slate-900 cursor-pointer hover:shadow-md transition-shadow ${
           !notification.isRead ? "ring-1 ring-indigo-200 dark:ring-indigo-800" : ""
         }`}
+        role="button"
+        tabIndex={0}
+        onKeyDown={(e) => {
+          if (e.key === "Enter" || e.key === " ") {
+            e.preventDefault();
+            handleClick();
+          }
+        }}
         onClick={handleClick}
       >
📝 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
<Card
className={`border-gray-200 dark:border-slate-800 bg-white dark:bg-slate-900 cursor-pointer hover:shadow-md transition-shadow ${
!notification.isRead ? "ring-1 ring-indigo-200 dark:ring-indigo-800" : ""
}`}
onClick={handleClick}
>
<CardContent className="flex items-start gap-4 py-4">
<Card
className={`border-gray-200 dark:border-slate-800 bg-white dark:bg-slate-900 cursor-pointer hover:shadow-md transition-shadow ${
!notification.isRead ? "ring-1 ring-indigo-200 dark:ring-indigo-800" : ""
}`}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
}}
onClick={handleClick}
>
<CardContent className="flex items-start gap-4 py-4">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/notifications/NotificationCenter.tsx` around lines 93 - 99, The
Card component in NotificationCenter.tsx with the onClick handler is only
accessible via pointer clicks, blocking keyboard users. Make the Card
keyboard-activatable by adding a tabIndex attribute to make it focusable, adding
an onKeyDown event handler to detect Enter or Space key presses and trigger the
same handleClick logic, and optionally adding a role attribute to indicate the
component is interactive. This ensures both pointer and keyboard users can open
notifications and perform actions.

Comment on lines +43 to +52
<Label className="text-sm font-medium text-gray-900 dark:text-gray-100">
{label}
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
</div>
<Switch checked={checked} onCheckedChange={onCheckedChange} />
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Associate each switch with its visible label.

The Switch controls currently have no explicit accessible name linkage (id/htmlFor), which can make preference toggles ambiguous for screen readers.

Suggested fix
-import { useState } from "react";
+import { useId, useState } from "react";
@@
 function ToggleRow({
@@
 }: ToggleRowProps) {
+  const switchId = useId();
   return (
@@
-          <Label className="text-sm font-medium text-gray-900 dark:text-gray-100">
+          <Label htmlFor={switchId} className="text-sm font-medium text-gray-900 dark:text-gray-100">
             {label}
           </Label>
@@
-      <Switch checked={checked} onCheckedChange={onCheckedChange} />
+      <Switch id={switchId} checked={checked} onCheckedChange={onCheckedChange} />
     </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
<Label className="text-sm font-medium text-gray-900 dark:text-gray-100">
{label}
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
</div>
<Switch checked={checked} onCheckedChange={onCheckedChange} />
</div>
import { useId, useState } from "react";
function ToggleRow({
label,
description,
checked,
onCheckedChange,
}: ToggleRowProps) {
const switchId = useId();
return (
<div className="flex items-center justify-between py-3">
<div className="flex flex-col gap-1">
<Label htmlFor={switchId} className="text-sm font-medium text-gray-900 dark:text-gray-100">
{label}
</Label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
</div>
<Switch id={switchId} checked={checked} onCheckedChange={onCheckedChange} />
</div>
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/notifications/NotificationPreferences.tsx` around lines 43 -
52, The Switch component in the notification preference item lacks explicit
association with its Label, making it inaccessible to screen readers. Add a
unique id attribute to the Label component and then link the Switch component to
that id using the aria-labelledby attribute to establish the proper accessible
relationship between the label and the switch toggle.

Comment on lines +58 to +63
const [localPrefs, setLocalPrefs] = useState<PrefsType>(preferences);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);

const hasChanges =
JSON.stringify(localPrefs) !== JSON.stringify(preferences);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync draft preferences when context preferences change.

Line [58] snapshots preferences only once. Because preferences are loaded asynchronously, the UI can display stale defaults and false “unsaved changes”.

Suggested fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
   const [localPrefs, setLocalPrefs] = useState<PrefsType>(preferences);
@@
+  useEffect(() => {
+    setLocalPrefs(preferences);
+  }, [preferences]);
📝 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 [localPrefs, setLocalPrefs] = useState<PrefsType>(preferences);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const hasChanges =
JSON.stringify(localPrefs) !== JSON.stringify(preferences);
const [localPrefs, setLocalPrefs] = useState<PrefsType>(preferences);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
useEffect(() => {
setLocalPrefs(preferences);
}, [preferences]);
const hasChanges =
JSON.stringify(localPrefs) !== JSON.stringify(preferences);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/notifications/NotificationPreferences.tsx` around lines 58 -
63, The `localPrefs` state is initialized only once with the `preferences` prop,
but when `preferences` changes asynchronously (such as when preferences are
loaded from the backend), the `localPrefs` state is not updated, causing the UI
to display stale defaults and false unsaved changes. Add a useEffect hook that
watches `preferences` as a dependency and updates `localPrefs` whenever the
`preferences` prop changes, ensuring the local state stays synchronized with the
async-loaded preferences.

Comment on lines +65 to +73
const handleSave = async () => {
setSaving(true);
try {
await updatePreferences(localPrefs);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} finally {
setSaving(false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t report save success when persistence fails.

Line [69] sets success after await updatePreferences(localPrefs), but the provider implementation logs and swallows errors, so this path can show “Saved!” even on write failure.

Suggested fix
   const handleSave = async () => {
     setSaving(true);
+    setSaved(false);
     try {
       await updatePreferences(localPrefs);
       setSaved(true);
       setTimeout(() => setSaved(false), 2000);
+    } catch {
+      setSaved(false);
     } finally {
       setSaving(false);
     }
   };

Also rethrow in src/dashboard/notifications/NotificationContext.tsx inside updatePreferences after logging so callers can react to failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/notifications/NotificationPreferences.tsx` around lines 65 -
73, The handleSave function in NotificationPreferences.tsx calls setSaved(true)
after awaiting updatePreferences, but since the updatePreferences function in
NotificationContext.tsx logs and swallows errors without rethrowing, the success
state is set even when persistence fails. Modify the updatePreferences function
in NotificationContext.tsx to rethrow the error after logging it, then update
the try-catch block in handleSave to properly handle the caught error so that
setSaved(true) only executes when the update actually succeeds.

Comment on lines +113 to +127
export async function markAllAsRead(userId: string): Promise<void> {
const q = query(
collection(db, NOTIFICATIONS_COLLECTION),
where("userId", "==", userId),
where("isRead", "==", false),
);
const snapshot = await getDocs(q);
if (snapshot.empty) return;

const batch = writeBatch(db);
snapshot.docs.forEach((docSnap) => {
batch.update(docSnap.ref, { isRead: true });
});
await batch.commit();
}

Copy link
Copy Markdown

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
# Description: Check for Firestore composite index configuration

# Look for firestore.indexes.json or firebase.json with index definitions
fd -e json -e js 'firestore.indexes|firebase' --exec cat {} \; | grep -A 10 -B 2 'notifications'

Repository: hrx01-dev/Servio

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for Firebase configuration files more broadly
echo "=== Searching for Firebase/Firestore config files ==="
find . -type f \( -name "firestore.indexes.json" -o -name "firebase.json" -o -name ".firebaserc" \) 2>/dev/null | head -20

echo ""
echo "=== Checking for any JSON files in root/config directories ==="
ls -la . 2>/dev/null | grep -E '\.(json|config)'
ls -la ./config 2>/dev/null || echo "No config directory found"
ls -la ./firebase 2>/dev/null || echo "No firebase directory found"

echo ""
echo "=== Looking for index definitions in the codebase ==="
rg -i "composite.*index|index.*notification|isRead.*userId" --type json --type js --type ts | head -20

echo ""
echo "=== Checking the project structure ==="
git ls-files | grep -E '(firebase|firestore)' | head -20

Repository: hrx01-dev/Servio

Length of output: 1170


🏁 Script executed:

#!/bin/bash
# Check if Firestore is actually being used and how indexes are managed
echo "=== Checking Firestore initialization and setup ==="
rg -i "initializeApp|getFirestore|firebaseConfig" --type ts --type js -A 3 | head -40

echo ""
echo "=== Looking for Firestore rules file ==="
find . -name "firestore.rules" 2>/dev/null

echo ""
echo "=== Checking package.json for Firebase version ==="
cat package.json | grep -A 5 -B 5 firebase

Repository: hrx01-dev/Servio

Length of output: 1837


🏁 Script executed:

#!/bin/bash
# Check the actual content of firestore.indexes.json
echo "=== Content of firestore.indexes.json ==="
cat firestore.indexes.json

echo ""
echo "=== Content of firebase.json ==="
cat firebase.json

Repository: hrx01-dev/Servio

Length of output: 1165


🏁 Script executed:

#!/bin/bash
# Verify the exact query in the notificationService.ts file
echo "=== Viewing the exact code in notificationService.ts lines 113-127 ==="
sed -n '113,127p' src/dashboard/notifications/notificationService.ts

Repository: hrx01-dev/Servio

Length of output: 559


Add composite index for the markAllAsRead query.

The query uses two where clauses on different fields (userId and isRead), which requires a composite index in Firestore. The index is currently missing from firestore.indexes.json. Without it, this query will fail at runtime.

Add the following index to firestore.indexes.json:

Composite index configuration
{
  "collectionGroup": "notifications",
  "queryScope": "COLLECTION",
  "fields": [
    { "fieldPath": "userId", "order": "ASCENDING" },
    { "fieldPath": "isRead", "order": "ASCENDING" }
  ]
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/notifications/notificationService.ts` around lines 113 - 127,
The markAllAsRead function performs a query on the notifications collection with
two where clauses filtering by userId and isRead fields, which requires a
composite index in Firestore. Add a composite index configuration to
firestore.indexes.json for the notifications collection with fields userId
(ascending) and isRead (ascending) to ensure the query on the
NOTIFICATIONS_COLLECTION will execute without runtime errors.

Comment on lines +154 to +161
export async function updatePreferences(
userId: string,
prefs: NotificationPreferences,
): Promise<void> {
const { setDoc } = await import("firebase/firestore");
const ref = doc(db, PREFERENCES_COLLECTION, userId);
await setDoc(ref, prefs, { merge: true });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove unnecessary dynamic import.

setDoc is available from the same firebase/firestore package already imported at the top of the file. The dynamic import is inconsistent with the rest of the codebase and adds unnecessary overhead.

♻️ Proposed fix
 import {
   collection,
   query,
   where,
   orderBy,
   onSnapshot,
   doc,
   updateDoc,
   addDoc,
   writeBatch,
   getDocs,
   Timestamp,
   limit,
   startAfter,
+  setDoc,
   type QueryDocumentSnapshot,
   type DocumentData,
 } from "firebase/firestore";
 export async function updatePreferences(
   userId: string,
   prefs: NotificationPreferences,
 ): Promise<void> {
-  const { setDoc } = await import("firebase/firestore");
   const ref = doc(db, PREFERENCES_COLLECTION, userId);
   await setDoc(ref, prefs, { merge: true });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/notifications/notificationService.ts` around lines 154 - 161,
The updatePreferences function contains an unnecessary dynamic import of setDoc
from firebase/firestore inside the function body. Since setDoc is already
imported at the top of the file from the same firebase/firestore package, remove
the dynamic import statement and simply use the already-imported setDoc directly
in the setDoc call within the function. This will eliminate the redundant import
and keep the code consistent with the rest of the codebase.

Comment thread src/Firebase/SignUp.tsx
Comment on lines 264 to 267
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider);
const { user } = await signInWithPopup(auth, provider);
notifyWelcome(user.uid, user.displayName ?? user.email ?? 'there');
navigate('/dashboard');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Duplicate welcome notifications for returning Google users.

signInWithPopup succeeds for both new and existing users. A returning user who uses the signup page will receive another welcome notification. Use getAdditionalUserInfo to check if the user is new before triggering the notification.

🐛 Proposed fix to only notify new users
+import { getAdditionalUserInfo } from 'firebase/auth';
 const handleGoogleSignIn = async () => {
   setError('');
   try {
     const provider = new GoogleAuthProvider();
-    const { user } = await signInWithPopup(auth, provider);
-    notifyWelcome(user.uid, user.displayName ?? user.email ?? 'there');
+    const result = await signInWithPopup(auth, provider);
+    const { user } = result;
+    if (getAdditionalUserInfo(result)?.isNewUser) {
+      notifyWelcome(user.uid, user.displayName ?? user.email ?? 'there');
+    }
     navigate('/dashboard');
   } catch (err: unknown) {
📝 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 provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider);
const { user } = await signInWithPopup(auth, provider);
notifyWelcome(user.uid, user.displayName ?? user.email ?? 'there');
navigate('/dashboard');
const provider = new GoogleAuthProvider();
const result = await signInWithPopup(auth, provider);
const { user } = result;
if (getAdditionalUserInfo(result)?.isNewUser) {
notifyWelcome(user.uid, user.displayName ?? user.email ?? 'there');
}
navigate('/dashboard');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Firebase/SignUp.tsx` around lines 264 - 267, The current implementation
calls notifyWelcome for all users who sign in via the GoogleAuthProvider,
including returning users. To fix this, use getAdditionalUserInfo from Firebase
to check if the user is new before sending the welcome notification. Destructure
the result of signInWithPopup to include the additional user info, then check
the isNewUser property from getAdditionalUserInfo before calling notifyWelcome.
Only invoke notifyWelcome when isNewUser is true to prevent duplicate
notifications for returning users.

@hrx01-dev hrx01-dev merged commit 30fe924 into main Jun 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant