Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
# Example .env.local
# Copy this file to .env.local and fill in the values for local development

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_secret_here

GITHUB_ID=your_github_id_here
GITHUB_SECRET=your_github_secret_here

MONGODB_URI=mongodb://localhost:27017
NODE_ENV=development

CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=

# Firebase (client) - used in the browser
NEXT_PUBLIC_FIREBASE_DATABASE_URL=
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
NEXT_PUBLIC_FIREBASE_VAPID_KEY=

# Firebase Admin (server) - used to send push notifications from the server
# If you paste a multi-line JSON private key here, replace newlines with \n
FIREBASE_PROJECT_ID=
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

ALL_GATHERING_ID=

GOOGLE_GENERATIVE_AI_API_KEY=

RZP_PROD_KEY_ID=
RZP_PROD_KEY_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_secret_here

Expand All @@ -12,6 +54,7 @@ CLOUDINARY_CLOUD_NAME=
CLOUDINARY_API_KEY=
CLOUDINARY_API_SECRET=

# Firebase Client Configuration
NEXT_PUBLIC_FIREBASE_DATABASE_URL=
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
Expand All @@ -20,6 +63,12 @@ NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSENGER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
NEXT_PUBLIC_FIREBASE_VAPID_KEY=

# Firebase Admin Configuration (for push notifications)
FIREBASE_PROJECT_ID=
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
Expand Down
21 changes: 21 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@

<!-- Describe the change in a single sentence. -->

Summary:

---

Checklist:
- [ ] I added/updated documentation where relevant (see `PUSH_NOTIFICATIONS.md`)
- [ ] I updated `.env.example` with any required env vars
- [ ] The app builds and starts locally (`npm run dev`) without secrets (guarded behavior)
- [ ] For features that require external services, I documented how to test (service worker/Firebase instructions)
- [ ] I did not commit secrets or private keys

Testing steps:
1. Run `npm run dev`
2. Open http://localhost:3000
3. Sign in and test notification flows when env vars are provided, or run the /api/notifications/test helper if present

Notes for reviewers:
- This PR includes push notification setup and defensive guards so local runs without Firebase/Mongo credentials do not crash the app.
# 🚀 Pull Request Overview

## 📄 Description
Expand Down
87 changes: 87 additions & 0 deletions PUSH_NOTIFICATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Push Notifications (FCM) — Setup & Testing

This document explains how to set up and test browser push notifications for CodeNearby using Firebase Cloud Messaging (FCM). It also contains notes for reviewers and a small PR checklist.

## Quick summary
- Client: FCM Web SDK (config via NEXT_PUBLIC_* env vars)
- Server: Firebase Admin SDK (requires service account credentials in env)
- Service worker: `public/firebase-messaging-sw.js` — must contain real Firebase config at build time

## Required env vars (local)
Fill these in `.env.local` (see `.env.example`):
- NEXT_PUBLIC_FIREBASE_API_KEY
- NEXT_PUBLIC_FIREBASE_PROJECT_ID
- NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
- NEXT_PUBLIC_FIREBASE_APP_ID
- NEXT_PUBLIC_FIREBASE_DATABASE_URL
- NEXT_PUBLIC_FIREBASE_VAPID_KEY

Server-side (for sending pushes):
- FIREBASE_PROJECT_ID
- FIREBASE_CLIENT_EMAIL
- FIREBASE_PRIVATE_KEY (replace newlines with `\\n` when pasting)

Also ensure `MONGODB_URI` is set and a running MongoDB is reachable for testing user token storage.

## Service worker
- `public/firebase-messaging-sw.js` currently contains placeholder config and comments.
- Service workers can't read process.env at runtime; replace the placeholders during build or deploy. Example techniques:
- Template + build script that injects NEXT_PUBLIC_FIREBASE_* values into the file
- Keep a separate `firebase-messaging-sw.prod.js` generated in CI

## Local testing steps
1. Populate `.env.local` with the env vars above and run `npm run dev`.
2. Open http://localhost:3000 in Chrome/Edge/Firefox (HTTPS is required for production; `localhost` is allowed during dev).
3. Sign in and open Profile → Edit → Notifications. Click "Enable notifications" and grant permission in the browser.
4. From another account or by using the test route (see below) send a message or friend request. Verify you receive a notification while:
- App is in foreground (in-app toast / native notification)
- App is backgrounded or closed (OS notification via service worker)
5. Click the notification to ensure it navigates to the expected page.

## Developer test helper (recommended for reviewers without Firebase keys)
If you want reviewers to try the UI without Firebase access, add a small temporary route that returns a simulated push payload to the client and opens the notification UI. This repo intentionally avoids shipping such a route; if you'd like, I can add a `/api/notifications/test` route that:
- Accepts an authenticated user
- Returns a simulated notification payload
- Does not call Firebase Admin

## PR checklist for this feature
- [ ] `.env.example` updated with required keys and clear instructions
- [ ] `public/firebase-messaging-sw.js` documented and note about build-time replacement included
- [ ] Server-side send functions gracefully no-op when admin credentials are missing (so reviewers can run the site without secrets)
- [ ] Unit or integration tests for the server send helper (optional)
- [ ] Documentation included (this file)

## Notes for reviewers
- The code contains guards around Firebase and Mongo initialization to avoid fatal startup errors when envs are missing. This is deliberate so contributors can run locally.
- Background notifications depend on your deployment injecting real Firebase config into the service worker at build time.
- Server sends will be no-ops if admin credentials are not present; check server logs for messages when testing with credentials.

---

If you'd like, I can:
- Add a small `/api/notifications/test` endpoint so reviewers can exercise the UI without Firebase credentials, or
- Implement a build-time script (simple Node script) to generate `public/firebase-messaging-sw.js` from env vars during `npm run build`.

Tell me which option you prefer and I'll implement it and open the PR.
## Future Enhancements

- 🔔 **Rich Notifications**: Add images, actions, and rich content
- 📊 **Analytics**: Track notification open rates and effectiveness
- 🎯 **Targeting**: Send notifications based on user activity/location
- 📱 **Mobile App**: Extend to native mobile app notifications
- 🔕 **Quiet Hours**: Respect user timezone and quiet hours
- 🔄 **Sync**: Better cross-device notification sync

## Contributing

When adding new notification types:

1. Add the notification type to `NotificationSettings` interface
2. Update server-side notification functions in `push-notifications-server.ts`
3. Add UI controls in `notification-settings.tsx`
4. Update service worker click handlers in `firebase-messaging-sw.js`
5. Test across different browsers and devices

---

Built with ❤️ for the CodeNearby community
15 changes: 15 additions & 0 deletions app/api/friends/request/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import clientPromise from "@/lib/mongodb";
import { authOptions } from "@/app/options";
import { sendFriendRequestNotification } from "@/lib/push-notifications-server";

export async function POST(request: Request) {
try {
Expand Down Expand Up @@ -52,6 +53,20 @@ export async function POST(request: Request) {
receiverInCodeNearby: !!receiverUser,
});

// Send push notification if the receiver is a CodeNearby user and has FCM token
if (receiverUser?.fcmToken && receiverUser?.notificationSettings?.friendRequests !== false) {
try {
await sendFriendRequestNotification(
receiverUser.fcmToken,
session.user.name || session.user.githubUsername || "Someone",
session.user.githubUsername || "unknown"
);
} catch (error) {
console.error("Error sending friend request push notification:", error);
// Don't fail the request if notification fails
}
}

return NextResponse.json({ id: result.insertedId });
} catch (error) {
console.error("Error creating friend request:", error);
Expand Down
47 changes: 45 additions & 2 deletions app/api/gathering/[slug]/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { authOptions } from "@/app/options";
import { ref, push } from "firebase/database";
import clientPromise from "@/lib/mongodb";
import { db as firebaseDb } from "@/lib/firebase";
import { sendGatheringMessageNotification } from "@/lib/push-notifications-server";

export async function POST(
request: Request,
Expand Down Expand Up @@ -55,8 +56,50 @@ export async function POST(
: null,
};

const messagesRef = ref(firebaseDb, `messages/${params.slug}`);
await push(messagesRef, message);
if (!firebaseDb) {
console.warn("Firebase database is not initialized. Skipping push to realtime DB.");
} else {
const messagesRef = ref(firebaseDb, `messages/${params.slug}`);
await push(messagesRef, message);
}

// Send push notifications to gathering participants (except sender)
try {
// Get gathering participants with FCM tokens
const participantsWithTokens = await db
.collection("users")
.find(
{
_id: { $in: gathering.participants.filter((id: string) => id !== session.user.id) },
fcmToken: { $exists: true, $ne: null },
"notificationSettings.gatheringMessages": { $ne: false },
},
{ projection: { fcmToken: 1 } }
)
.toArray();

const fcmTokens = participantsWithTokens
.map((user) => user.fcmToken)
.filter(Boolean);

if (fcmTokens.length > 0) {
// Create message preview (limit to 100 characters)
const messagePreview = content.length > 100
? content.substring(0, 97) + "..."
: content;

await sendGatheringMessageNotification(
fcmTokens,
isAnonymous ? "Anonymous" : (session.user.name || "Someone"),
gathering.title || "Gathering",
messagePreview,
params.slug
);
}
} catch (error) {
console.error("Error sending gathering message push notifications:", error);
// Don't fail the request if notification fails
}

return NextResponse.json({ success: true });
} catch (error) {
Expand Down
70 changes: 70 additions & 0 deletions app/api/messages/notify/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/options";
import clientPromise from "@/lib/mongodb";
import { sendMessageNotification } from "@/lib/push-notifications-server";

export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { recipientId, content, chatId } = await request.json();

if (!recipientId || !content || !chatId) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 }
);
}

const client = await clientPromise;
const db = client.db();

// Get recipient user data
const recipientUser = await db
.collection("users")
.findOne({ githubId: parseInt(recipientId) });

if (!recipientUser) {
return NextResponse.json(
{ error: "Recipient not found" },
{ status: 404 }
);
}

// Check if recipient has notifications enabled and has FCM token
const shouldSendNotification =
recipientUser.fcmToken &&
recipientUser.notificationSettings?.messages !== false;

if (shouldSendNotification) {
try {
// Create message preview (limit to 100 characters)
const messagePreview = content.length > 100
? content.substring(0, 97) + "..."
: content;

await sendMessageNotification(
recipientUser.fcmToken,
session.user.name || session.user.githubUsername || "Someone",
messagePreview,
chatId
);
} catch (error) {
console.error("Error sending message push notification:", error);
// Don't fail the request if notification fails
}
}

return NextResponse.json({ success: true });
} catch (error) {
console.error("Error processing message notification:", error);
return NextResponse.json(
{ error: "Failed to process notification" },
{ status: 500 }
);
}
}
Loading