feat: integrate Firebase Cloud Messaging for push notifications
Some checks failed
Build frontend / build (push) Failing after 24s

This commit is contained in:
Claw AI
2026-03-31 19:50:48 +00:00
parent 81674c4aa7
commit 2bea2d190c
6 changed files with 1213 additions and 1 deletions

View File

@ -41,6 +41,7 @@ import { motion, AnimatePresence } from "framer-motion";
import AuthService from "./services/AuthService"; import AuthService from "./services/AuthService";
import { UserRole, UserRoleLabels } from "./enums/UserRole"; import { UserRole, UserRoleLabels } from "./enums/UserRole";
import "./i18n/config"; import "./i18n/config";
import NotificationHandler from "./components/NotificationHandler";
export default function ClientLayout({ children }) { export default function ClientLayout({ children }) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
@ -798,6 +799,7 @@ export default function ClientLayout({ children }) {
</div> </div>
</footer> </footer>
)} )}
<NotificationHandler />
</> </>
); );
} }

View File

@ -0,0 +1,53 @@
"use client";
import { useEffect, useState } from "react";
import { requestNotificationPermission, onForegroundMessage } from "../utils/firebase";
export default function NotificationHandler() {
const [notification, setNotification] = useState(null);
useEffect(() => {
// Request permission and get token
requestNotificationPermission().then((token) => {
if (token) {
console.log("[Notifications] FCM token obtained");
// TODO: Send token to your backend to register the device
// e.g. apiFetch('/Notifications/RegisterDevice', { method: 'POST', body: { token } })
}
});
// Listen for foreground messages
const unsubscribe = onForegroundMessage((payload) => {
const title = payload.notification?.title || payload.data?.title || "Sweet Home";
const body = payload.notification?.body || payload.data?.body || "";
setNotification({ title, body });
// Auto-dismiss after 5 seconds
setTimeout(() => setNotification(null), 5000);
});
return () => unsubscribe();
}, []);
if (!notification) return null;
return (
<div className="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 bg-white rounded-xl shadow-2xl border border-gray-200 p-4 z-[9999] animate-slide-up">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center flex-shrink-0">
<span className="text-xl">🏠</span>
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-gray-900 text-sm">{notification.title}</p>
<p className="text-gray-600 text-sm mt-0.5">{notification.body}</p>
</div>
<button
onClick={() => setNotification(null)}
className="text-gray-400 hover:text-gray-600 flex-shrink-0"
>
</button>
</div>
</div>
);
}

63
app/utils/firebase.js Normal file
View File

@ -0,0 +1,63 @@
import { initializeApp, getApps } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
const firebaseConfig = {
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
authDomain: "sweet-home-b2766.firebaseapp.com",
projectId: "sweet-home-b2766",
storageBucket: "sweet-home-b2766.firebasestorage.app",
messagingSenderId: "602865114600",
appId: "1:602865114600:web:ed9b6754940507a6ab585d",
measurementId: "G-M2V95NBJLX",
};
// Initialize Firebase (avoid duplicate init in SSR)
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
// Get messaging instance (only works in browser)
let messaging = null;
if (typeof window !== "undefined" && "serviceWorker" in navigator) {
try {
messaging = getMessaging(app);
} catch (e) {
console.warn("[Firebase] Messaging init failed:", e.message);
}
}
// Request notification permission and get FCM token
export async function requestNotificationPermission() {
if (typeof window === "undefined") return null;
try {
const permission = await Notification.requestPermission();
if (permission !== "granted") {
console.log("[FCM] Notification permission denied");
return null;
}
const registration = await navigator.serviceWorker.register("/firebase-messaging-sw.js");
const token = await getToken(messaging, {
vapidKey: "BO0tGzMOqN3xQp8IG2wQEXwJKUJfx7T3eVvLq3HjC2Q", // TODO: Replace with your VAPID key from Firebase Console
serviceWorkerRegistration: registration,
});
console.log("[FCM] Token:", token);
return token;
} catch (err) {
console.error("[FCM] Error getting token:", err);
return null;
}
}
// Listen for foreground messages
export function onForegroundMessage(callback) {
if (!messaging) return () => {};
return onMessage(messaging, (payload) => {
console.log("[FCM] Foreground message:", payload);
callback(payload);
});
}
export { app, messaging };

1057
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
}, },
"dependencies": { "dependencies": {
"@pbe/react-yandex-maps": "^1.2.5", "@pbe/react-yandex-maps": "^1.2.5",
"firebase": "^12.11.0",
"flowbite": "^4.0.1", "flowbite": "^4.0.1",
"flowbite-react": "^0.12.16", "flowbite-react": "^0.12.16",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",

View File

@ -0,0 +1,38 @@
// Firebase Cloud Messaging Service Worker
// This file MUST be in the public/ directory (served at /firebase-messaging-sw.js)
importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js");
firebase.initializeApp({
apiKey: "AIzaSyBZV7KBLRJSTApahfrO8lBesmIM3zNRSaY",
authDomain: "sweet-home-b2766.firebaseapp.com",
projectId: "sweet-home-b2766",
storageBucket: "sweet-home-b2766.firebasestorage.app",
messagingSenderId: "602865114600",
appId: "1:602865114600:web:ed9b6754940507a6ab585d",
measurementId: "G-M2V95NBJLX",
});
const messaging = firebase.messaging();
// Handle background messages
messaging.onBackgroundMessage((payload) => {
console.log("[FCM SW] Background message:", payload);
const title = payload.notification?.title || payload.data?.title || "Sweet Home";
const options = {
body: payload.notification?.body || payload.data?.body || "",
icon: payload.notification?.icon || "/logo.png",
badge: "/logo.png",
data: payload.data,
tag: "sweethome-notification",
};
self.registration.showNotification(title, options);
});
// Handle notification click
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const url = event.notification.data?.url || "/";
event.waitUntil(clients.openWindow(url));
});