Files
Messenger/web/src/utils/firebasePush.ts
Codex 148870de14
Some checks failed
Android CI / android (push) Has started running
Android Release / release (push) Has been cancelled
CI / test (push) Has been cancelled
web: guard invalid VAPID key during push subscription
2026-03-10 00:35:06 +03:00

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;
}