import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createClient } from 'npm:@supabase/supabase-js@2'; import { cert, getApps, initializeApp } from 'npm:firebase-admin@12.2.0/app'; import { getMessaging } from 'npm:firebase-admin@12.2.0/messaging'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; // Singleton — Edge Functions can be warm-reused; avoid "app already exists" error const app = getApps()[0] ?? initializeApp({ credential: cert( JSON.parse( atob(Deno.env.get('FIREBASE_SERVICE_ACCOUNT')!.replace(/\s/g, '')) ) ), }); const messaging = getMessaging(app); serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); } try { const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY')!; const authHeader = req.headers.get('Authorization'); if (!authHeader) { return new Response(JSON.stringify({ error: 'Missing auth headers' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } const supabase = createClient(supabaseUrl, supabaseAnonKey, { global: { headers: { Authorization: authHeader } }, }); const body = await req.json(); const { userId, title, body: messageBody, type, orderId, voucherId, priority = 'important', persist = true, delaySeconds, } = body; if (!userId || !title || !messageBody || !type) { return new Response( JSON.stringify({ error: 'Missing required parameters' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, } ); } if (delaySeconds && delaySeconds > 0) { await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)); } // Persist notification if needed (using service role key to insert securely if needed, but here auth is user) // Wait, if an admin/webhook is calling this, they will use service_role. If a user is calling, they use their token. // Let's create a service-role client just for DB updates to ensure it works even if sender is different from recipient const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const adminSupabase = createClient(supabaseUrl, serviceRoleKey); let notificationId: string | null = null; let persisted = false; if (persist) { const { data: notifData, error: notifError } = await adminSupabase .from('notifications') .insert({ user_id: userId, title, body: messageBody, type, priority, order_id: orderId || null, voucher_id: voucherId || null, }) .select('id') .single(); if (!notifError && notifData) { notificationId = notifData.id; persisted = true; } } // Get user's profile to find fcm_token and preferences const { data: profile } = await adminSupabase .from('profiles') .select('fcm_token, notification_push_enabled') .eq('id', userId) .single(); let sent = false; if (profile && profile.notification_push_enabled && profile.fcm_token) { const message = { notification: { title: title, body: messageBody, }, data: { orderId: orderId || '', voucherId: voucherId || '', type: type || '', }, apns: { payload: { aps: { badge: 1, sound: 'default', }, }, }, android: { notification: { sound: 'default', notificationCount: 1, }, }, token: profile.fcm_token, }; try { await messaging.send(message); sent = true; } catch (err: any) { if ( err.code === 'messaging/registration-token-not-registered' || err.code === 'messaging/invalid-registration-token' ) { // Token is stale — remove it from the DB await adminSupabase .from('profiles') .update({ fcm_token: null }) .eq('id', userId); } console.error('FCM send error:', err); } } return new Response( JSON.stringify({ ok: true, sent, persisted, notificationId }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, } ); } catch (err: any) { return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } });