170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
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' },
|
|
});
|
|
}
|
|
});
|