import { createClient } from 'npm:@supabase/supabase-js@2' import { JWT } from 'npm:google-auth-library@9' // CRITICAL: We MUST use the static file import to prevent the 546 CPU crash import serviceAccount from '../service-account.json' with { type: 'json' } interface Notification { id: string user_id: string type: string ticket_id: string | null task_id: string | null } interface WebhookPayload { type: 'INSERT' table: string record: Notification schema: 'public' } const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) Deno.serve(async (req) => { try { const payload: WebhookPayload = await req.json() // 1. Get all tokens for this user const { data: tokenData, error } = await supabase .from('fcm_tokens') .select('token') .eq('user_id', payload.record.user_id) if (error || !tokenData || tokenData.length === 0) { console.log('No active FCM tokens found for user:', payload.record.user_id) return new Response('No tokens', { status: 200 }) } // 2. Auth with Google using the statically loaded JSON const accessToken = await getAccessToken({ clientEmail: serviceAccount.client_email, privateKey: serviceAccount.private_key, }) // 3. Build the notification text const notificationTitle = `New ${payload.record.type}` let notificationBody = 'You have a new update in TasQ.' if (payload.record.ticket_id) { notificationBody = 'You have a new update on your ticket.' } else if (payload.record.task_id) { notificationBody = 'You have a new update on your task.' } // 4. Send to all devices concurrently const sendPromises = tokenData.map(async (row) => { const res = await fetch( `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ message: { token: row.token, // ❌ REMOVED the top-level 'notification' block entirely! data: { title: notificationTitle, // ✅ Moved title here body: notificationBody, // ✅ Moved body here notification_id: payload.record.id, type: payload.record.type, ticket_id: payload.record.ticket_id || '', task_id: payload.record.task_id || '', }, // Android priority (keep this to wake the device) android: { priority: 'high', }, // iOS must STILL use the apns block for background processing apns: { payload: { aps: { sound: 'tasq_notification.wav', contentAvailable: 1, }, }, }, }, }), } ) const resData = await res.json() // 5. Automatic Cleanup: If Firebase says the token is dead, delete it from the DB if (!res.ok && resData.error?.details?.[0]?.errorCode === 'UNREGISTERED') { console.log(`Dead token detected. Removing from DB: ${row.token}`) await supabase.from('fcm_tokens').delete().eq('token', row.token) } return { token: row.token, status: res.status, response: resData } }) const results = await Promise.all(sendPromises) return new Response(JSON.stringify(results), { headers: { 'Content-Type': 'application/json' }, }) } catch (err) { console.error('FCM Error:', err) return new Response(JSON.stringify({ error: String(err) }), { status: 500 }) } }) // JWT helper const getAccessToken = ({ clientEmail, privateKey, }: { clientEmail: string privateKey: string }): Promise => { return new Promise((resolve, reject) => { const jwtClient = new JWT({ email: clientEmail, key: privateKey, scopes: ['https://www.googleapis.com/auth/firebase.messaging'], }) jwtClient.authorize((err, tokens) => { if (err) { reject(err) return } resolve(tokens!.access_token!) }) }) }