152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
import { initializeApp, getApps } from "firebase/app";
|
|
import { getMessaging, getToken, isSupported, onMessage, type MessagePayload } from "firebase/messaging";
|
|
import { deletePushToken, upsertPushToken } from "../api/notifications";
|
|
import { showNotificationViaServiceWorker } from "./webNotifications";
|
|
|
|
const WEB_PUSH_TOKEN_KEY = "bm_web_push_token";
|
|
|
|
let foregroundListenerAttached = false;
|
|
|
|
export async function ensureWebPushRegistration(): Promise<void> {
|
|
const config = getFirebaseConfig();
|
|
const vapidKey = normalizeVapidKey(import.meta.env.VITE_FIREBASE_VAPID_KEY);
|
|
if (!config || !vapidKey) {
|
|
return;
|
|
}
|
|
if (!(await isSupported())) {
|
|
return;
|
|
}
|
|
if (!("Notification" in window)) {
|
|
return;
|
|
}
|
|
if (Notification.permission === "default") {
|
|
await Notification.requestPermission();
|
|
}
|
|
if (Notification.permission !== "granted") {
|
|
return;
|
|
}
|
|
|
|
if (!("serviceWorker" in navigator)) {
|
|
return;
|
|
}
|
|
|
|
const registration = await navigator.serviceWorker.ready;
|
|
const app = getApps()[0] ?? initializeApp(config);
|
|
const messaging = getMessaging(app);
|
|
let token: string | null = null;
|
|
try {
|
|
token = await getToken(messaging, {
|
|
vapidKey,
|
|
serviceWorkerRegistration: registration,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof DOMException && error.name === "InvalidAccessError") {
|
|
console.error(
|
|
"[web-push] Invalid VAPID key format. Check VITE_FIREBASE_VAPID_KEY in web env.",
|
|
);
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
if (!token) {
|
|
return;
|
|
}
|
|
|
|
const previous = window.localStorage.getItem(WEB_PUSH_TOKEN_KEY);
|
|
if (previous && previous !== token) {
|
|
await deletePushToken({ platform: "web", token: previous }).catch(() => undefined);
|
|
}
|
|
await upsertPushToken({ platform: "web", token, app_version: "web" });
|
|
window.localStorage.setItem(WEB_PUSH_TOKEN_KEY, token);
|
|
|
|
if (!foregroundListenerAttached) {
|
|
foregroundListenerAttached = true;
|
|
onMessage(messaging, (payload) => {
|
|
void showForegroundNotification(payload);
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function unregisterWebPushToken(): Promise<void> {
|
|
const token = window.localStorage.getItem(WEB_PUSH_TOKEN_KEY);
|
|
if (!token) {
|
|
return;
|
|
}
|
|
await deletePushToken({ platform: "web", token }).catch(() => undefined);
|
|
window.localStorage.removeItem(WEB_PUSH_TOKEN_KEY);
|
|
}
|
|
|
|
async function showForegroundNotification(payload: MessagePayload): Promise<void> {
|
|
const data = payload.data ?? {};
|
|
const chatId = Number(data.chat_id);
|
|
const messageId = Number(data.message_id);
|
|
const title = payload.notification?.title ?? "New message";
|
|
const body = payload.notification?.body ?? "Open chat";
|
|
if (Number.isFinite(chatId) && Number.isFinite(messageId)) {
|
|
await showNotificationViaServiceWorker({
|
|
chatId,
|
|
messageId,
|
|
title,
|
|
body,
|
|
});
|
|
}
|
|
}
|
|
|
|
function getFirebaseConfig():
|
|
| {
|
|
apiKey: string;
|
|
authDomain?: string;
|
|
projectId: string;
|
|
storageBucket?: string;
|
|
messagingSenderId: string;
|
|
appId: string;
|
|
}
|
|
| null {
|
|
const apiKey = import.meta.env.VITE_FIREBASE_API_KEY?.trim();
|
|
const projectId = import.meta.env.VITE_FIREBASE_PROJECT_ID?.trim();
|
|
const messagingSenderId = import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID?.trim();
|
|
const appId = import.meta.env.VITE_FIREBASE_APP_ID?.trim();
|
|
if (!apiKey || !projectId || !messagingSenderId || !appId) {
|
|
return null;
|
|
}
|
|
return {
|
|
apiKey,
|
|
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN?.trim() || undefined,
|
|
projectId,
|
|
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET?.trim() || undefined,
|
|
messagingSenderId,
|
|
appId,
|
|
};
|
|
}
|
|
|
|
function normalizeVapidKey(raw: string | undefined): string | null {
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
let key = raw.trim();
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
|
|
// Accept accidental JSON payloads copied from docs/panels.
|
|
if (key.startsWith("{") && key.endsWith("}")) {
|
|
try {
|
|
const parsed = JSON.parse(key) as { vapidKey?: string; publicKey?: string; key?: string };
|
|
key = (parsed.vapidKey ?? parsed.publicKey ?? parsed.key ?? "").trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Strip wrapping quotes and whitespace/newlines.
|
|
key = key.replace(/^['"]|['"]$/g, "").replace(/\s+/g, "");
|
|
|
|
// Convert classic base64 chars to URL-safe format expected by PushManager/Firebase.
|
|
key = key.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
|
|
if (!/^[A-Za-z0-9\-_]+$/.test(key)) {
|
|
return null;
|
|
}
|
|
return key.length >= 80 ? key : null;
|
|
}
|