/** * Native FCM push for the bundled Capacitor app (Phase B) — rings the device on a new chat DM when * the app is backgrounded/killed (the live WebSocket only covers an OPEN app; web-push doesn't reach * a native app). * * App-only + staff-only + fail-soft: * - early-returns on the web build (__CAP_BUILD__ undefined) and the plugin is also dropped from the * web bundle by the nuxt.config app:resolve hook; * - @capacitor/push-notifications is DYNAMICALLY imported so the web build never loads it; * - registers only once the user is eligible staff (useChatEligible — same gate as the chat socket); * - any failure (no google-services.json, perm denied, …) is swallowed → native push just stays off. * * NOTE: the filename intentionally avoids the substring 'push-notifications' — the app:resolve drop * list strips that (it targets the WEB VAPID plugin 02.push-notifications.client.ts). */ import { toast } from 'bulma-toast' // Injected by Vite only in the app build (see nuxt.config.ts). declare const __CAP_BUILD__: boolean | undefined export default defineNuxtPlugin(() => { if (typeof __CAP_BUILD__ === 'undefined') return // web build → no-op if (!process.client) return if (useRuntimeConfig().public.chatEnabled !== true) return const eligible = useChatEligible() // Captured at plugin time: push listeners fire later, outside any Nuxt // context, so useState-backed composables must run via runWithContext. const nuxtApp = useNuxtApp() const feedback = useFeedback() let registered = false const setup = async () => { if (registered || !eligible.value) return registered = true try { const { PushNotifications } = await import('@capacitor/push-notifications') // Token issued → persist it for this staff user (server resolves the user from the bearer). PushNotifications.addListener('registration', (token: any) => { $fetch('/api/push/register-native', { method: 'POST', body: { token: token?.value, platform: 'android' }, }).catch((e: any) => console.warn('[native-push] token register failed:', e?.message)) }) PushNotifications.addListener('registrationError', (err: any) => console.warn('[native-push] registration error:', err?.error)) // Tap on a notification → open that conversation, or a pushed task's ticket. PushNotifications.addListener('pushNotificationActionPerformed', (action: any) => { const data = action?.notification?.data || {} if (data.type === 'pushed_task' && data.ticketId) { navigateTo(`/mobile/tasks?pushedTaskId=${data.ticketId}`) return } const sender = data.senderUserId navigateTo(sender ? `/chat?chat=${sender}` : '/chat') }) // Foreground receipt: chat stays a no-op (the live WS renders it), but a // pushed task must surface on the dashboard right away — Android hands a // foreground FCM message to the app WITHOUT a tray notification, so without // this the worker saw nothing until they left and re-entered the dashboard. PushNotifications.addListener('pushNotificationReceived', (n: any) => { const data = n?.data || {} if (data.type !== 'pushed_task') return Promise.resolve(nuxtApp.runWithContext(() => fetchPushedTasks())).catch(() => {}) feedback.warning() toast({ message: `📌 Neue Aufgabe #${data.docNo || ''}`, type: 'is-danger', duration: 4000, position: 'top-center' }) }) // High-importance channel so chat pushes can heads-up on Android 8+ (FCM targets channelId 'chat'). try { await PushNotifications.createChannel({ id: 'chat', name: 'Chat', importance: 5, visibility: 1 }) } catch { /* iOS / unsupported */ } let perm = await PushNotifications.checkPermissions() if (perm.receive === 'prompt' || perm.receive === 'prompt-with-rationale') { perm = await PushNotifications.requestPermissions() } if (perm.receive !== 'granted') { registered = false; return } // allow a later retry await PushNotifications.register() } catch (e: any) { registered = false console.warn('[native-push] setup failed:', e?.message) } } // Register as soon as the user is eligible (immediately if already logged in, else after login). watch(eligible, (v) => { if (v) setup() }, { immediate: true }) })