feat: implement pwa push notifications

This commit is contained in:
2026-01-13 09:03:46 +01:00
parent e7291951d8
commit 14430b275e
16 changed files with 547 additions and 50 deletions

View File

@@ -21,3 +21,51 @@ export async function sendNotification(webhookUrl: string | null, message: strin
console.error("Failed to send notification:", error);
}
}
import { sendPushNotification } from "./push";
import prisma from "@/lib/prisma";
export async function sendPlanNotification(planId: string, message: string, webhookUrl?: string | null) {
// Parallelize sending
const promises: Promise<any>[] = [];
if (webhookUrl) {
promises.push(sendNotification(webhookUrl, message));
}
try {
const subscriptions = await prisma.pushSubscription.findMany({
where: { planId }
});
if (subscriptions.length > 0) {
const payload = {
title: "Cat Sitting Planner",
body: message,
url: `/`
// We could pass specific URL if needed, but for now root is okay or dashboard?
// The service worker opens the URL.
// Ideally, we want to open `/dashboard/[planId]`.
};
// Refine URL in payload
// We need 'lang'. We don't have it here easily unless passed.
// But we can guess or just link to root and let redirect handle it?
// Or just link to context.
// Let's rely on SW opening `/`.
subscriptions.forEach(sub => {
promises.push((async () => {
const res = await sendPushNotification(sub, payload);
if (!res.success && (res.statusCode === 410 || res.statusCode === 404)) {
await prisma.pushSubscription.delete({ where: { id: sub.id } });
}
})());
});
}
} catch (e) {
console.error("Failed to fetch/send push subscriptions", e);
}
await Promise.allSettled(promises);
}

37
lib/push.ts Normal file
View File

@@ -0,0 +1,37 @@
import webpush from 'web-push';
if (!process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) {
console.warn("VAPID keys are missing. Push notifications will not work.");
} else {
webpush.setVapidDetails(
process.env.VAPID_SUBJECT || 'mailto:admin@localhost',
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
}
interface PushSubscriptionData {
endpoint: string;
p256dh: string;
auth: string;
}
export async function sendPushNotification(subscription: PushSubscriptionData, payload: any) {
try {
await webpush.sendNotification({
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.p256dh,
auth: subscription.auth,
}
}, JSON.stringify(payload));
return { success: true, statusCode: 201 };
} catch (error: any) {
if (error.statusCode === 410 || error.statusCode === 404) {
// Subscription is gone
return { success: false, statusCode: error.statusCode };
}
console.error("Error sending push:", error);
return { success: false, statusCode: error.statusCode || 500 };
}
}