PageSourceSearch

https://escute.ai/firebase-messaging-sw.js

js escute.ai collected 2026-10-05 14:38:59 UTC 4,599 bytes, 107 lines download raw bytes

1/* Firebase Cloud Messaging service worker (push web do Escute.ai).
2 * Recebe mensagens em segundo plano e mostra a notificação. As chaves abaixo são
3 * PÚBLICAS (config web do Firebase) — não há segredo aqui. Coexiste com /sw.js
4 * (PWA) por usar escopo próprio do FCM. */
5importScripts('https://www.gstatic.com/firebasejs/10.12.2/firebase-app-compat.js')
6importScripts('https://www.gstatic.com/firebasejs/10.12.2/firebase-messaging-compat.js')
7
8firebase.initializeApp({
9  apiKey: 'AIzaSyDY6NY_9xcdddV5zbXW2KkI7lhSrMIQkSo',
10  authDomain: 'escute-ai.firebaseapp.com',
11  projectId: 'escute-ai',
12  storageBucket: 'escute-ai.firebasestorage.app',
13  messagingSenderId: '776325154142',
14  appId: '1:776325154142:web:6ae018f7aeeb246d777bcf',
15  measurementId: 'G-SSH42E8784',
16})
17
18const messaging = firebase.messaging()
19
20// Convite de voz: notificação com ações. Chega data-only (o servidor omite o
21// bloco `notification` justamente para o SW poder desenhá-la com botões).
22const VOICE_INVITE_TYPE = 'voice_invite'
23
24// Mensagens em segundo plano (aba fechada/sem foco).
25messaging.onBackgroundMessage(payload => {
26  const title = payload.notification?.title || payload.data?.title || 'Escute.ai'
27  const body = payload.notification?.body || payload.data?.body || ''
28  const url = payload.fcmOptions?.link || payload.data?.path || '/home'
29  // Fase 3 (persona): push do caderno manda data.icon = avatar da voz atual —
30  // no web o ícone da notificação É a "foto do remetente". Genérico mantém a
31  // marca (icon-192).
32  const icon = payload.data?.icon || '/icons/icon-192.png'
33
34  if (payload.data?.type === VOICE_INVITE_TYPE && payload.data?.inviteId && payload.data?.deliveryId) {
35    const inviteId = payload.data.inviteId
36    // Identidade da AÇÃO. Sem ela o servidor não consegue distinguir este
37    // aparelho dos outros que receberam o mesmo convite, e a garantia de
38    // sessão única cai — por isso o convite sem `deliveryId` nem é desenhado.
39    const deliveryId = payload.data.deliveryId
40    self.registration.showNotification(title, {
41      body,
42      icon,
43      badge: '/icons/icon-192.png',
44      // `actions` é ignorado silenciosamente onde não há suporte — nesse caso
45      // sobra o corpo inteiro, que equivale a Atender. Degradação, não quebra.
46      actions: [
47        { action: 'answer', title: 'Atender' },
48        { action: 'decline', title: 'Agora não' },
49      ],
50      data: { url, inviteId, deliveryId, voiceInvite: true },
51      vibrate: [150, 50, 150],
52      // Sem `renotify`: a reconciliação de um envio perdido reenvia o
53      // MESMO convite, e renotify faria o segundo alertar de novo. A tag
54      // estável substitui a notificação em silêncio — um alerta, nunca dois.
55      tag: `voice-invite-${inviteId}`,
56      requireInteraction: false,
57    })
58    return
59  }
60
61  self.registration.showNotification(title, {
62    body,
63    icon,
64    badge: '/icons/icon-192.png',
65    data: { url },
66    vibrate: [150, 50, 150],
67    requireInteraction: false,
68  })
69})
70
71self.addEventListener('notificationclick', event => {
72  event.notification.close()
73  const data = event.notification.data || {}
74  const url = data.url || '/home'
75
76  // AGORA NÃO: não abre janela, não navega, não inicia nada. Só informa o
77  // servidor, que é a autoridade sobre o desfecho. Falha de rede não vira
78  // recusa local — o convite fica como está e a varredura o encerra.
79  if (data.voiceInvite && data.inviteId && data.deliveryId && event.action === 'decline') {
80    event.waitUntil(
81      fetch(`/api/agent/voice-invites/${data.inviteId}/decline`, {
82        method: 'POST',
83        headers: { 'Content-Type': 'application/json' },
84        credentials: 'same-origin',
85        body: JSON.stringify({ platform: 'web', deliveryId: data.deliveryId }),
86      }).catch(() => undefined)
87    )
88    return
89  }
90
91  // Qualquer outro caminho — botão Atender OU toque no corpo — abre o destino.
92  event.waitUntil(
93    self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(list => {
94      const existing = list.find(c => c.url.includes(url))
95      if (existing) {
96        // Já há janela aberta: navega para o destino do convite em vez de só
97        // focar. Focar sem navegar deixaria a pessoa na tela em que estava, e
98        // o convite morreria sem desfecho.
99        if (data.voiceInvite && 'navigate' in existing) {
100          return existing.navigate(url).then(c => (c || existing).focus()).catch(() => existing.focus())
101        }
102        return existing.focus()
103      }
104      return self.clients.openWindow(url)
105    })
106  )
107})

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.