upd: web push

Deskripsi:
- install package
- table database
- nb : masih blm bisa

No Issues
This commit is contained in:
amel
2024-09-18 11:48:57 +08:00
parent 6868085bb1
commit bc0f27a4fc
16 changed files with 581 additions and 3 deletions

View File

@@ -0,0 +1,17 @@
import { NotificationManager } from "@/module/_global/components/notification_manager";
const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!;
console.log(
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
export default function Page() {
return (
<div>
{/* <PushNotificationManager publicKey={publicKey} /> */}
<NotificationManager publicKey={publicKey} />
</div>
);
}

View File

@@ -0,0 +1,6 @@
import prisma from "@/lib/prisma";
export async function GET() {
const sub = await prisma.subscription.findMany();
return new Response(JSON.stringify({ data: sub }));
}

View File

@@ -0,0 +1,70 @@
import webpush from "web-push";
import prisma from "@/lib/prisma";
// Set VAPID details for web-push
webpush.setVapidDetails(
"mailto:bip.production.js@gmail.com",
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
export async function POST() {
try {
// Fetch all subscriptions from your database
const subscriptions = await prisma.subscription.findMany();
if (!subscriptions || subscriptions.length === 0) {
console.error("No subscriptions available to send notification");
return new Response("No subscriptions available", { status: 400 });
}
// Notification payload
const notificationPayload = JSON.stringify({
title: "Test Notification",
body: "This is a test notification | makuro",
icon: "/icon-192x192.png",
badge: "/icon-192x192.png",
image: "/icon-192x192.png",
});
let successCount = 0;
let failureCount = 0;
// Loop through all subscriptions and send notifications
for (const sub of subscriptions) {
try {
const subscriptionData = sub.data as any;
await webpush.sendNotification(subscriptionData, notificationPayload);
console.log(
`Notification sent successfully to ${subscriptionData.endpoint}`
);
successCount++;
} catch (error: any) {
console.error(
`Error sending push notification to subscription ${sub.id}:`,
error
);
failureCount++;
}
}
// Return a success or failure response
return new Response(
JSON.stringify({
message: `Notifications sent: ${successCount}, Failed: ${failureCount}`,
success: failureCount === 0
}),
{ status: 200 }
);
} catch (error: any) {
console.error("Error during notification process:", error);
return new Response(
JSON.stringify({
success: false,
error: error.message || "Failed to process notifications"
}),
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,25 @@
import webpush from "web-push";
import prisma from "@/lib/prisma";
webpush.setVapidDetails(
"mailto:bip.production.js@gmail.com",
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
export async function POST(req: Request) {
const { sub } = await req.json();
console.log(sub);
if (!sub || !sub.endpoint) {
console.error("Invalid subscription object");
return new Response("Invalid subscription object", { status: 400 });
}
const data = await prisma.subscription.create({
data: {
id: sub.keys.auth,
data: sub
}
});
return new Response(JSON.stringify({ data }));
}

View File

@@ -0,0 +1,11 @@
import prisma from "@/lib/prisma";
export async function POST(req: Request) {
const { sub } = await req.json();
const data = await prisma.subscription.delete({
where: {
id: sub.keys.auth
}
});
return new Response(JSON.stringify({ data }));
}

29
src/app/manifest.ts Normal file
View File

@@ -0,0 +1,29 @@
import type { MetadataRoute } from "next";
export default function manifest(): MetadataRoute.Manifest {
return {
name: "Sistem Desa Mandiri",
short_name: "SDM",
description: "Sistem Desa Mandiri",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#000000",
icons: [
{
src: "/icon-192x192.png",
sizes: "192x192",
type: "image/png"
},
{
src: "/icon-512x512.png",
sizes: "512x512",
type: "image/png"
}
],
serviceworker: {
src: "/wibu_worker.js"
},
};
}

15
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,15 @@
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
}
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
export default prisma
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma

View File

@@ -0,0 +1,13 @@
export const urlB64ToUint8Array = (base64String: string) => {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};

46
src/lib/usePWAInstall.ts Normal file
View File

@@ -0,0 +1,46 @@
import { useState, useEffect } from "react";
export function usePWAInstall() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const [isAppInstalled, setIsAppInstalled] = useState(false);
useEffect(() => {
const beforeInstallHandler = (e: any) => {
e.preventDefault();
setDeferredPrompt(e);
};
const appInstalledHandler = () => {
setIsAppInstalled(true);
};
window.addEventListener("beforeinstallprompt", beforeInstallHandler);
window.addEventListener("appinstalled", appInstalledHandler);
return () => {
window.removeEventListener("beforeinstallprompt", beforeInstallHandler);
window.removeEventListener("appinstalled", appInstalledHandler);
};
}, []);
const handleInstallClick = () => {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult: any) => {
if (choiceResult.outcome === "accepted") {
console.log("User accepted the install prompt");
} else {
console.log("User dismissed the install prompt");
}
setDeferredPrompt(null);
});
}
};
return {
deferredPrompt,
isAppInstalled,
handleInstallClick,
};
}

View File

@@ -0,0 +1,75 @@
import { useState, useEffect } from "react";
import { urlB64ToUint8Array } from "./urlB64ToUint8Array";
export function usePushNotifications(publicKey: string) {
const [isSupported, setIsSupported] = useState(false);
const [subscription, setSubscription] = useState<PushSubscription | null>(
null
);
useEffect(() => {
if ("serviceWorker" in navigator && "PushManager" in window) {
setIsSupported(true);
registerServiceWorker();
}
}, []);
const registerServiceWorker = async () => {
try {
const registration = await navigator.serviceWorker.register(
"/wibu_worker.js",
{
scope: "/",
updateViaCache: "none"
}
);
const sub = await registration.pushManager.getSubscription();
if (sub) {
setSubscription(sub);
}
} catch (error) {
console.error("Service Worker registration failed:", error);
}
};
const subscribeToPush = async () => {
try {
const registration = await navigator.serviceWorker.ready;
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlB64ToUint8Array(publicKey)
});
const res = await fetch("/api/set-subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sub: sub.toJSON() })
});
if (res.ok) {
setSubscription(sub);
}
} catch (error) {
console.error("Subscription error:", error);
}
};
const unsubscribeFromPush = async () => {
if (subscription) {
await subscription.unsubscribe();
setSubscription(null);
await fetch("/api/unsubscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sub: subscription.toJSON() })
});
}
};
return {
isSupported,
subscription,
subscribeToPush,
unsubscribeFromPush
};
}

View File

@@ -0,0 +1,83 @@
"use client";
import { usePushNotifications } from "@/lib/usePushNotifications";
import { usePWAInstall } from "@/lib/usePWAInstall";
import { useState } from "react";
// test v1
export function NotificationManager({ publicKey }: { publicKey: string }) {
const {
isSupported,
subscription,
subscribeToPush,
unsubscribeFromPush
} = usePushNotifications(publicKey);
const { deferredPrompt, isAppInstalled, handleInstallClick } =
usePWAInstall();
const [message, setMessage] = useState("halo apa kabar");
const sendTestNotification = async () => {
if (!subscription) return;
try {
const res = await fetch("/api/send-notification", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sub: subscription.toJSON(), message })
});
if (!res.ok) {
console.error("Failed to send notification:", res.statusText);
}
} catch (error) {
console.error("Notification error:", error);
}
};
if (!isSupported) {
return <p>Push notifications are not supported in this browser.</p>;
}
return (
<div>
<h3>Push Notifications & PWA Install</h3>
{subscription ? (
<>
<p>You are subscribed to push notifications.</p>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px"
}}
>
<div>
<button onClick={unsubscribeFromPush}>Unsubscribe</button>
</div>
<input
type="text"
placeholder="Enter notification message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<div>
<button onClick={sendTestNotification}>
Send Test Notification
</button>
</div>
</div>
</>
) : (
<>
<p>You are not subscribed to push notifications.</p>
<button onClick={subscribeToPush}>Subscribe</button>
</>
)}
<hr />
{!isAppInstalled && deferredPrompt && (
<button onClick={handleInstallClick}>Install App</button>
)}
</div>
);
}