/** * Solicita permissão para notificações desktop */ export async function requestNotificationPermission(): Promise { if (!("Notification" in window)) { console.warn("Este navegador não suporta notificações desktop"); return "denied"; } if (Notification.permission === "granted") { return "granted"; } if (Notification.permission !== "denied") { return await Notification.requestPermission(); } return Notification.permission; } /** * Mostra uma notificação desktop */ export function showNotification(title: string, options?: NotificationOptions): Notification | null { if (!("Notification" in window)) { return null; } if (Notification.permission !== "granted") { return null; } try { return new Notification(title, { icon: "/favicon.png", badge: "/favicon.png", ...options, }); } catch (error) { console.error("Erro ao exibir notificação:", error); return null; } } /** * Toca o som de notificação */ export function playNotificationSound() { try { const audio = new Audio("/sounds/notification.mp3"); audio.volume = 0.5; audio.play().catch((err) => { console.warn("Não foi possível reproduzir o som de notificação:", err); }); } catch (error) { console.error("Erro ao tocar som de notificação:", error); } } /** * Verifica se o usuário está na aba ativa */ export function isTabActive(): boolean { return !document.hidden; } /** * Registrar service worker para push notifications */ export async function registrarServiceWorker(): Promise { if (!("serviceWorker" in navigator)) { console.warn("Service Workers não são suportados neste navegador"); return null; } try { // Verificar se já existe um Service Worker ativo antes de registrar const existingRegistration = await navigator.serviceWorker.getRegistration("/"); if (existingRegistration?.active) { return existingRegistration; } // Registrar com timeout para evitar travamentos const registerPromise = navigator.serviceWorker.register("/sw.js", { scope: "/", }); const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 3000) ); const registration = await Promise.race([registerPromise, timeoutPromise]); if (registration) { // Log apenas em desenvolvimento if (import.meta.env.DEV) { console.log("Service Worker registrado:", registration); } } return registration; } catch (error) { // Ignorar erros silenciosamente para evitar spam no console // especialmente erros relacionados a message channel if (error instanceof Error) { const errorMessage = error.message.toLowerCase(); if ( !errorMessage.includes("message channel") && !errorMessage.includes("registration") && import.meta.env.DEV ) { console.error("Erro ao registrar Service Worker:", error); } } return null; } } /** * Solicitar subscription de push notification */ export async function solicitarPushSubscription(): Promise { try { // Registrar service worker primeiro com timeout const registrationPromise = registrarServiceWorker(); const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 3000) ); const registration = await Promise.race([registrationPromise, timeoutPromise]); if (!registration) { return null; } // Verificar se push está disponível if (!("PushManager" in window)) { return null; } // Solicitar permissão com timeout const permissionPromise = requestNotificationPermission(); const permissionTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve("denied"), 3000) ); const permission = await Promise.race([permissionPromise, permissionTimeoutPromise]); if (permission !== "granted") { return null; } // Obter subscription existente ou criar nova com timeout const getSubscriptionPromise = registration.pushManager.getSubscription(); const getSubscriptionTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 3000) ); let subscription = await Promise.race([getSubscriptionPromise, getSubscriptionTimeoutPromise]); if (!subscription) { // VAPID public key deve vir do backend ou config const vapidPublicKey = import.meta.env.VITE_VAPID_PUBLIC_KEY || ""; if (!vapidPublicKey) { // Não logar warning para evitar spam no console return null; } // Converter chave para formato Uint8Array const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey); // Subscribe com timeout const subscribePromise = registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey, }); const subscribeTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 5000) ); subscription = await Promise.race([subscribePromise, subscribeTimeoutPromise]); } return subscription; } catch (error) { // Ignorar erros relacionados a message channel ou service worker if (error instanceof Error) { const errorMessage = error.message.toLowerCase(); if ( errorMessage.includes("message channel") || errorMessage.includes("service worker") || errorMessage.includes("registration") ) { return null; } } return null; } } /** * Converter chave VAPID de base64 URL-safe para Uint8Array */ function urlBase64ToUint8Array(base64String: string): Uint8Array { const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } /** * Converter PushSubscription para formato serializável */ export function subscriptionToJSON(subscription: PushSubscription): { endpoint: string; keys: { p256dh: string; auth: string }; } { const key = subscription.getKey("p256dh"); const auth = subscription.getKey("auth"); if (!key || !auth) { throw new Error("Chaves de subscription não encontradas"); } return { endpoint: subscription.endpoint, keys: { p256dh: arrayBufferToBase64(key), auth: arrayBufferToBase64(auth), }, }; } /** * Converter ArrayBuffer para base64 */ function arrayBufferToBase64(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); let binary = ""; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return window.btoa(binary); } /** * Remover subscription de push notification */ export async function removerPushSubscription(): Promise { const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (subscription) { await subscription.unsubscribe(); return true; } return false; }