upd: web push
Deskripsi: - install package - table database - nb : masih blm bisa No Issues
This commit is contained in:
17
src/app/(application)/web-push/page.tsx
Normal file
17
src/app/(application)/web-push/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
6
src/app/api/get-subscribe/route.ts
Normal file
6
src/app/api/get-subscribe/route.ts
Normal 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 }));
|
||||
}
|
||||
70
src/app/api/send-notification/route.ts
Normal file
70
src/app/api/send-notification/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
25
src/app/api/set-subscribe/route.ts
Normal file
25
src/app/api/set-subscribe/route.ts
Normal 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 }));
|
||||
}
|
||||
11
src/app/api/unsubscribe/route.ts
Normal file
11
src/app/api/unsubscribe/route.ts
Normal 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
29
src/app/manifest.ts
Normal 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
15
src/lib/prisma.ts
Normal 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
|
||||
13
src/lib/urlB64ToUint8Array.ts
Normal file
13
src/lib/urlB64ToUint8Array.ts
Normal 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
46
src/lib/usePWAInstall.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
75
src/lib/usePushNotifications.ts
Normal file
75
src/lib/usePushNotifications.ts
Normal 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
|
||||
};
|
||||
}
|
||||
83
src/module/_global/components/notification_manager.tsx
Normal file
83
src/module/_global/components/notification_manager.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user