feat: integrate Firebase Cloud Messaging for push notifications
Some checks failed
Build frontend / build (push) Failing after 24s
Some checks failed
Build frontend / build (push) Failing after 24s
This commit is contained in:
@ -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 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
53
app/components/NotificationHandler.js
Normal file
53
app/components/NotificationHandler.js
Normal 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
63
app/utils/firebase.js
Normal 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
1057
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
|||||||
38
public/firebase-messaging-sw.js
Normal file
38
public/firebase-messaging-sw.js
Normal 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));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user