141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
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<string> => {
|
|
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!)
|
|
})
|
|
})
|
|
} |